react-hooks如何使用?

1. 什么是react-hooks?

** react-hooks是react16.8之后,react新增的钩子API,目的是增长代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。笔者认为,react-hooks思想和初衷,也是把组件,颗粒化,单元化,造成独立的渲染环境,减小渲染次数,优化性能。react

useCallback✅es6

useContext✅编程

useEffect✅redux

useLayoutEffect ✅api

useMemo ✅数组

useReducer✅promise

useRef✅浏览器

useState✅ 以上就是react-hooks主要的api,接下来我会和你们分享一下这些api的用法,以及使用他们的注意事项。缓存

2.为何要使用hooks

咱们为何要使用react-hooks呢,首先和传统的class声明的有状态有这显著的优势就是性能优化

1 react-hooks可让咱们的代码的逻辑性更强,能够抽离公共的方法,公共组件。

2 react-hooks思想更趋近于函数式编程。用函数声明方式代替class声明方式,虽然说class也是es6构造函数语法糖,可是react-hooks写起来更有函数即组件,无疑也提升代码的开发效率(无需像class声明组件那样写声明周期,写生命周期render函数等)

3 react-hooks可能把庞大的class组件,化整为零成不少小组件,useMemo等方法让组件或者变量制定一个适合本身的独立的渲染空间,必定程度上能够提升性能,减小渲染次数。这里值得一提的是,若是把负责 请求是数据 ➡️ 视图更新的渲染组件,用react-hooks编写的话 ,配合immutable等优秀的开源库,会有更棒的效果(这里特别注意的是⚠️,若是乱用hooks,不但不会提高性能,反而会影响性能,带来各类各样的想不到的问题)。

3.如何使用hooks

接下来和你们探讨一下,react-hooks主要api,具体使用

1 useState 数据存储,派发更新

useState出现,使得react无状态组件可以像有状态组件同样,能够拥有本身state,useState的参数能够是一个具体的值,也能够是一个函数用于判断复杂的逻辑,函数返回做为初始值,usestate 返回一个数组,数组第一项用于读取此时的state值 ,第二项为派发数据更新,组件渲染的函数,函数的参数便是须要更新的值。useState和useReduce 做为可以触发组件从新渲染的hooks,咱们在使用useState的时候要特别注意的是,useState派发更新函数的执行,就会让整个function组件从头至尾执行一次,因此须要配合useMemo,usecallback等api配合使用,这就是我说的为何滥用hooks会带来负做用的缘由之一了。一下代码为usestate基本应用

const DemoState = (props) => {
   /* number为此时state读取值 ,setNumber为派发更新的函数 */
   let [number, setNumber] = useState(0) /* 0为初始值 */
   return (<div> <span>{ number }</span> <button onClick={ ()=> { setNumber(number+1) console.log(number) /* 这里的number是不可以即便改变的 */ } } ></button> </div>)
}
复制代码

上边简单的例子说明了useState ,可是当咱们在调用更新函数以后,state的值是不能即时改变的,只有当下一次上下文执行的时候,state值才随之改变。

const a =1 
const DemoState = (props) => {
   /* useState 第一个参数若是是函数 则处理复杂的逻辑 ,返回值为初始值 */
   let [number, setNumber] = useState(()=>{
      // number
      return a===1 ? 1 : 2
   }) /* 1为初始值 */
   return (<div> <span>{ number }</span> <button onClick={ ()=>setNumber(number+1) } ></button> </div>)
}
复制代码

2 useEffect 组件更新反作用钩子

若是你想在function组件中,当组件完成挂载,dom渲染完成,作一些操纵dom,请求数据,那么useEffect是一个不二选择,若是咱们须要在组件初次渲染的时候请求数据,那么useEffect能够充当class组件中的 componentDidMount , 可是特别注意的是,若是不给useEffect执行加入限定条件,函数组件每一次更新都会触发effect ,那么也就说明每一次state更新,或是props的更新都会触发useEffect执行,此时的effect又充当了componentDidUpdate和componentwillreceiveprops,因此说合理的用于useEffect就要给effect加入限定执行的条件,也就是useEffect的第二个参数,这里说是限定条件,也能够说是上一次useeffect更新收集的某些记录数据变化的记忆,在新的一轮更新,useeffect会拿出以前的记忆值和当前值作对比,若是发生了变化就执行新的一轮useEffect的反作用函数,useEffect第二个参数是一个数组,用来收集多个限制条件 。

/* 模拟数据交互 */
function getUserInfo(a){
    return new Promise((resolve)=>{
        setTimeout(()=>{ 
           resolve({
               name:a,
               age:16,
           }) 
        },500)
    })
}

const Demo = ({ a }) => {
    const [ userMessage , setUserMessage ] :any= useState({})
    const div= useRef()
    const [number, setNumber] = useState(0)
    /* 模拟事件监听处理函数 */
    const handleResize =()=>{}
    /* useEffect使用 ,这里若是不加限制 ,会是函数重复执行,陷入死循环*/
    useEffect(()=>{
        /* 请求数据 */
       getUserInfo(a).then(res=>{
           setUserMessage(res)
       })
       /* 操做dom */
       console.log(div.current) /* div */
       /* 事件监听等 */
        window.addEventListener('resize', handleResize)
    /* 只有当props->a和state->number改变的时候 ,useEffect反作用函数从新执行 ,若是此时数组为空[],证实函数只有在初始化的时候执行一次至关于componentDidMount */
    },[ a ,number ])
    return (<div ref={div} > <span>{ userMessage.name }</span> <span>{ userMessage.age }</span> <div onClick={ ()=> setNumber(1) } >{ number }</div> </div>)
}

复制代码

若是咱们须要在组件销毁的阶段,作一些取消dom监听,清除定时器等操做,那么咱们能够在useEffect函数第一个参数,结尾返回一个函数,用于清除这些反作用。至关与componentWillUnmount。

const Demo = ({ a }) => {
    /* 模拟事件监听处理函数 */
    const handleResize =()=>{}
    useEffect(()=>{
       /* 定时器 延时器等 */
       const timer = setInterval(()=>console.log(666),1000)
       /* 事件监听 */
       window.addEventListener('resize', handleResize)
       /* 此函数用于清除反作用 */
       return function(){
           clearInterval(timer) 
           window.removeEventListener('resize', handleResize)
       }
    },[ a ])
    return (<div > </div>)
}

复制代码

异步 async effect ?

提醒你们的是 useEffect是不能直接用 async await 语法糖的

/* 错误用法 ,effect不支持直接 async await 装饰的 */
 useEffect(async ()=>{
        /* 请求数据 */
      const res = await getUserInfo(payload)
    },[ a ,number ])

复制代码

若是咱们想要用 async effect 能够对effect进行一层包装

const asyncEffect = (callback, deps)=>{
   useEffect(()=>{
       callback()
   },deps)
}
复制代码

3useLayoutEffect 渲染更新以前的 useEffect

useEffect 执行顺序 组件更新挂载完成 -> 浏览器dom 绘制完成 -> 执行useEffect回调 。

useLayoutEffect 执行顺序 组件更新挂载完成 -> 执行useLayoutEffect回调-> 浏览器dom 绘制完成
因此说useLayoutEffect 代码可能会阻塞浏览器的绘制 若是咱们在useEffect 从新请求数据,渲染视图过程当中,确定会形成画面闪动的效果,而若是用useLayoutEffect ,回调函数的代码就会阻塞浏览器绘制,因此可定会引发画面卡顿等效果,那么具体要用 useLayoutEffect 仍是 useEffect ,要看实际项目的状况,大部分的状况 useEffect 均可以知足的。

const DemoUseLayoutEffect = () => {
    const target = useRef()
    useLayoutEffect(() => {
        /*咱们须要在dom绘制以前,移动dom到制定位置*/
        const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */
        animate(target.current,{ x,y })
    }, []);
    return (
        <div > <span ref={ target } className="animate"></span> </div>
    )
}
复制代码

4 useRef 获取元素 ,缓存数据。

和传统的class组件ref同样,react-hooks 也提供获取元素方法 useRef,它有一个参数能够做为缓存数据的初始值,返回值能够被dom元素ref标记,能够获取被标记的元素节点.

const DemoUseRef = ()=>{
    const dom= useRef(null)
    const handerSubmit = ()=>{
        /* <div >表单组件</div> dom 节点 */
        console.log(dom.current)
    }
    return <div> {/* ref 标记当前dom节点 */} <div ref={dom} >表单组件</div> <button onClick={()=>handerSubmit()} >提交</button> </div>
}
复制代码

高阶用法 缓存数据

固然useRef还有一个很重要的做用就是缓存数据,咱们知道usestate ,useReducer 是能够保存当前的数据源的,可是若是它们更新数据源的函数执行一定会带来整个组件重新执行到渲染,若是在函数组件内部声明变量,则下一次更新也会重置,若是咱们想要悄悄的保存数据,而又不想触发函数的更新,那么useRef是一个很棒的选择。

** const currenRef = useRef(InitialData)

获取 currenRef.current 改变 currenRef.current = newValue

useRef能够第一个参数能够用来初始化保存数据,这些数据能够在current属性上获取到 ,固然咱们也能够经过对current赋值新的数据源。

下面咱们经过react-redux源码来看看useRef的巧妙运用 (react-redux 在react-hooks发布后,用react-hooks从新了其中的Provide,connectAdvanced)核心模块,能够见得 react-hooks在限制数据更新,高阶组件上有这必定的优点,其源码大量运用useMemo来作数据断定

/* 这里用到的useRef没有一个是绑定在dom元素上的,都是作数据缓存用的 */
      /* react-redux 用userRef 来缓存 merge以后的 props */
      const lastChildProps = useRef()
      // lastWrapperProps 用 useRef 来存放组件真正的 props信息
      const lastWrapperProps = useRef(wrapperProps)
      //是否储存props是否处于正在更新状态
      const renderIsScheduled = useRef(false)
复制代码

这是react-redux中用useRef 对数据作的缓存,那么怎么作更新的呢 ,咱们接下来看

//获取包装的props 
function captureWrapperProps( lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, actualChildProps, childPropsFromStoreUpdate, notifyNestedSubs ) {
   //咱们要捕获包装props和子props,以便稍后进行比较
  lastWrapperProps.current = wrapperProps  //子props 
  lastChildProps.current = actualChildProps //通过 merge props 以后造成的 prop
  renderIsScheduled.current = false

}
复制代码

经过上面咱们能够看到 ,react-redux 用从新赋值的方法,改变缓存的数据源,避免没必要要的数据更新, 若是选用useState储存数据,必然促使组件从新渲染 因此采用了useRef解决了这个问题,至于react-redux源码怎么实现的,咱们这里能够参考笔者的另一篇文章react-redux源码解析

5 useContext 自由获取context

咱们可使用useContext ,来获取父级组件传递过来的context值,这个当前值就是最近的父级组件 Provider 设置的value值,useContext参数通常是由 createContext 方式引入 ,也能够父级上下文context传递 ( 参数为context )。useContext 能够代替 context.Consumer 来获取Provider中保存的value值

/* 用useContext方式 */
const DemoContext = ()=> {
    const value:any = useContext(Context)
    /* my name is alien */
return <div> my name is { value.name }</div>
}

/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
    return <Context.Consumer> {/* my name is alien */} { (value)=> <div> my name is { value.name }</div> } </Context.Consumer>
}

export default ()=>{
    return <div> <Context.Provider value={{ name:'alien' , age:18 }} > <DemoContext /> <DemoContext1 /> </Context.Provider> </div>
}
复制代码

6 useReducer 无状态组件中的redux

useReducer 是react-hooks提供的可以在无状态组件中运行的相似redux的功能api,至于它到底能不能代替redux react-redux ,我我的的见解是不能的 ,redux 可以复杂的逻辑中展示优点 ,并且 redux的中间件模式思想也是很是优秀了,咱们能够经过中间件的方式来加强dispatch redux-thunk redux-sage redux-action redux-promise都是比较不错的中间件,能够把同步reducer编程异步的reducer。useReducer 接受的第一个参数是一个函数,咱们能够认为它就是一个reducer ,reducer的参数就是常规reducer里面的state和action,返回改变后的state, useReducer第二个参数为state的初始值 返回一个数组,数组的第一项就是更新以后state的值 ,第二个参数是派发更新的dispatch函数 。dispatch 的触发会触发组件的更新,这里可以促使组件重新的渲染的一个是useState派发更新函数,另外一个就 useReducer中的dispatch

const DemoUseReducer = ()=>{
    /* number为更新后的state值, dispatchNumbner 为当前的派发函数 */
   const [ number , dispatchNumbner ] = useReducer((state,action)=>{
       const { payload , name  } = action
       /* return的值为新的state */
       switch(name){
           case 'add':
               return state + 1
           case 'sub':
               return state - 1 
           case 'reset':
             return payload       
       }
       return state
   },0)
   return <div> 当前值:{ number } { /* 派发更新 */ } <button onClick={()=>dispatchNumbner({ name:'add' })} >增长</button> <button onClick={()=>dispatchNumbner({ name:'sub' })} >减小</button> <button onClick={()=>dispatchNumbner({ name:'reset' ,payload:666 })} >赋值</button> { /* 把dispatch 和 state 传递给子组件 */ } <MyChildren dispatch={ dispatchNumbner } State={{ number }} /> </div>
}
复制代码

固然实际业务逻辑可能更复杂的,须要咱们在reducer里面作更复杂的逻辑操做。

7 useMemo 小而香性能优化

useMemo我认为是React设计最为精妙的hooks之一,优势就是能造成独立的渲染空间,可以使组件,变量按照约定好规则更新。渲染条件依赖于第二个参数deps。 咱们知道无状态组件的更新是从头至尾的更新,若是你想要重新渲染一部分视图,而不是整个组件,那么用useMemo是最佳方案,避免了不须要的更新,和没必要要的上下文的执行,在介绍useMemo以前,咱们先来讲一说, memo, 咱们知道class声明的组件能够用componentShouldUpdate来限制更新次数,那么memo就是无状态组件的ShouldUpdate , 而咱们今天要讲的useMemo就是更为细小的ShouldUpdate单元,

先来看看memo ,memo的做用结合了pureComponent纯组件和 componentShouldUpdate功能,会对传进来的props进行一次对比,而后根据第二个函数返回值来进一步判断哪些props须要更新。

/* memo包裹的组件,就给该组件加了限制更新的条件,是否更新取决于memo第二个参数返回的boolean值, */
const DemoMemo = connect(state =>
    ({ goodList: state.goodList })
)(memo(({ goodList, dispatch, }) => {
    useEffect(() => {
        dispatch({
            name: 'goodList',
        })
    }, [])
    return <Select placeholder={'请选择'} style={{ width: 200, marginRight: 10 }} onChange={(value) => setSeivceId(value)} > { goodList.map((item, index) => <Option key={index + 'asd' + item.itemId} value={item.itemId} > {item.itemName} </Option>) } </Select>
    /* 判断以前的goodList 和新的goodList 是否相等,若是相等, 则不更新此组件 这样就能够制定属于本身的渲染约定 ,让组件只有知足预约的下才从新渲染 */
}, (pre, next) => is(pre.goodList, next.goodList)))
复制代码

useMemo的应用理念和memo差很少,都是断定是否知足当前的限定条件来决定是否执行useMemo的callback函数,而useMemo的第二个参数是一个deps数组,数组里的参数变化决定了useMemo是否更新回调函数,useMemo返回值就是通过断定更新的结果。它能够应用在元素上,应用在组件上,也能够应用在上下文当中。若是又一个循环的list元素,那么useMemo会是一个不二选择,接下来咱们一块儿探寻一下useMemo的优势

/* 用 useMemo包裹的list能够限定当且仅当list改变的时候才更新此list,这样就能够避免selectList从新循环 */
 {useMemo(() => (
      <div>{ selectList.map((i, v) => ( <span className={style.listSpan} key={v} > {i.patentName} </span> ))} </div>
), [selectList])}
复制代码

1 useMemo能够减小没必要要的循环,减小没必要要的渲染

useMemo(() => (
    <Modal width={'70%'} visible={listshow} footer={[ <Button key="back" >取消</Button>, <Button key="submit" type="primary" > 肯定 </Button> ]} > { /* 减小了PatentTable组件的渲染 */ } <PatentTable getList={getList} selectList={selectList} cacheSelectList={cacheSelectList} setCacheSelectList={setCacheSelectList} /> </Modal>
 ), [listshow, cacheSelectList])
复制代码

2 useMemo能够减小子组件的渲染次数

const DemoUseMemo=()=>{
  /* 用useMemo 包裹以后的log函数能够避免了每次组件更新再从新声明 ,能够限制上下文的执行 */
    const newLog = useMemo(()=>{
        const log =()=>{
            console.log(6666)
        }
        return log
    },[])
    return <div onClick={()=>newLog()} ></div>
}
复制代码

3 useMemo让函数在某个依赖项改变的时候才运行,这能够避免不少没必要要的开销(这里要注意⚠️⚠️⚠️的是若是被useMemo包裹起来的上下文,造成一个独立的闭包,会缓存以前的state值,若是没有加相关的更新条件,是获取不到更新以后的state的值的,以下边👇⬇️)

const DemoUseMemo=()=>{
    const [ number ,setNumber ] = useState(0)
    const newLog = useMemo(()=>{
        const log =()=>{
            /* 点击span以后 打印出来的number 不是实时更新的number值 */
            console.log(number)
        }
        return log
      /* [] 没有 number */  
    },[])
    return <div> <div onClick={()=>newLog()} >打印</div> <span onClick={ ()=> setNumber( number + 1 ) } >增长</span> </div>
}

复制代码

useMemo很不错,react-redux 用react-hooks重写后运用了大量的useMemo情景,我为你们分析两处

useMemo 同过 store didStoreComeFromProps contextValue 属性制定是否须要重置更新订阅者subscription ,这里我就不为你们讲解react-redux了,有兴趣的同窗能够看看react-redux源码,看看是怎么用useMemo的

const [subscription, notifyNestedSubs] = useMemo(() => {
  if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY

  const subscription = new Subscription(
    store,
    didStoreComeFromProps ? null : contextValue.subscription // old 
  )
  
  const notifyNestedSubs = subscription.notifyNestedSubs.bind(
    subscription
  )

  return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])
复制代码

react-redux经过 判断 redux store的改变来获取与之对应的state

const previousState = useMemo(() => store.getState(), [store])
复制代码

讲到这里,若是咱们应用useMemo根据依赖项合理的颗粒化咱们的组件,能起到很棒的优化组件的做用。

8 useCallback useMemo版本的回调函数

useMemo和useCallback接收的参数都是同样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于useMemo返回的是函数运行的结果,useCallback返回的是函数,这个回调函数是通过处理后的也就是说父组件传递一个函数给子组件的时候,因为是无状态组件每一次都会从新生成新的props函数,这样就使得每一次传递给子组件的函数都发生了变化,这时候就会触发子组件的更新,这些更新是没有必要的,此时咱们就能够经过usecallback来处理此函数,而后做为props传递给子组件

/* 用react.memo */
const DemoChildren = React.memo((props)=>{
   /* 只有初始化的时候打印了 子组件更新 */
    console.log('子组件更新')
   useEffect(()=>{
       props.getInfo('子组件')
   },[])
   return <div>子组件</div>
})

const DemoUseCallback=({ id })=>{
    const [number, setNumber] = useState(1)
    /* 此时usecallback的第一参数 (sonName)=>{ console.log(sonName) } 通过处理赋值给 getInfo */
    const getInfo  = useCallback((sonName)=>{
          console.log(sonName)
    },[id])
    return <div> {/* 点击按钮触发父组件更新 ,可是子组件没有更新 */} <button onClick={ ()=>setNumber(number+1) } >增长</button> <DemoChildren getInfo={getInfo} /> </div>
}

复制代码

这里应该提醒的是,useCallback ,必须配合 react.memo pureComponent ,不然不但不会提高性能,还有可能下降性能

4总结

react-hooks的诞生,也不是说它可以彻底代替class声明的组件,对于业务比较复杂的组件,class组件仍是首选,只不过咱们能够把class组件内部拆解成funciton组件,根据业务需求,哪些负责逻辑交互,哪些须要动态渲染,而后配合usememo等api,让性能提高起来。react-hooks使用也有一些限制条件,好比说不能放在流程控制语句中,执行上下文也有必定的要求。整体来讲,react-hooks仍是很不错的,值得你们去学习和探索。

微信扫码关注公众号,按期分享技术文章

在这里插入图片描述