2

今年五月份就开始接触react hook,六月份也在组内分享过一次,但由于太忙所以现在才抽出时间写这篇关于hook的学习手册。

前沿:

react hook相信对于前端开发者来说也不会陌生,就是基于react16.8之后推出的解决函数式组件没办法处理内部状态持久化的一种解决方式,它能大大的提高了函数式组件的在react之中的地位。本文主要介绍react hook钩子的一些使用方式,至于原理性的东西后续补上。

背景:

在介绍react hook之前,还是先介绍一下为啥会出现react hook,使用它之后的函数式组件与类组件之间相比存在哪些优点。

  • class组件存在的问题:

1)多个类组件之间的逻辑较难复用:

如果多个组件之间存在相同的逻辑或者相类似的逻辑,一般会采取两种方式来进行复用,一种是通过高阶组件HOC来处理,另一种则是采用了render prop的方式。

例如一个场景为A组件是点击按钮,然后出现一个弹窗;B组件是文本,鼠标移入后出现一个弹窗;

image.png

从上面可以看出handle要被复用,还要再绕一圈通过HOC,而且还要装饰器,才能复用handle,代码的复杂度上升且代码量也多了。

2)复杂组件很难理解:

很多时候类组件一开始比较简单,而几个月后组件'变胖'了,render主体越来越多,绑定和调用的方法也越来越多,越来越多生命周期被调用而且生命周期内部的逻辑越来越复杂,有时候如监听函数的绑定和解除需要拆分成两部分放在两个生命周期等,原来组件才一百多行代码,一下子变成上千行代码。
image.png

3)class组件的生命周期使用时的坑:

相信很多开发者在使用类组件的时候,会经常在调用生命周期时踩过不少的坑,这里我大概总结了一下生命周期的坑:

image.png

  • class组件和函数式组件的对比:

如表中所示,因为函数式组件功能单一,以至于在hooks出来之前,一般不常用函数式组件。

image.png

  • hooks钩子能解决什么:

先对比一下类组件和运用了react hook的函数式组件,从图中可以清晰的看到类组件该有的东西,函数式组件运用了钩子之后才拥有了。

image.png

再用一个例子简单的对比一下,表格中列举了两个组件,然后分别用普通方式,HOC方式,react hook方式,可以看出代码的可读性,简洁性以及复用性钩子会更优异一点。

image.png

所以简单的总结了react hook的几个适用场景:

1)让对于复杂庞大的组件,可以拆分成颗粒度更小的函数式组件,每个小组件各自控制自身的状态;

2)将一段业务代码封装成复用,提供给每个组件;

常用api:

前面阐述了这么多,主要时让大家知道react hook的存在意义以及和类组件之间的对比,接下来要重点介绍一下react hook常用的几个钩子。

  • useState:

作用:可以让函数式组件中加入简单的状态管理,而且可以使用多次useState,但每个状态之间是相互独立,即更新一个状态,另一个状态不会随着也更新,但要注意只能在函数式组件中使用。

语法:
[状态,状态更新函数] = useState(状态默认值)
这里的状态和状态更新函数没有具体的变量声明,所以可以采用各种各样的声明变量。

注意点:
1)每更新一次状态,函数式组件就会重新在渲染一次;
2)利用状态改变函数改变了状态后,该状态不会立即同步,还是原来的值,只有rerender时,该状态才会最新;
3)状态改变函数是直接将新的值代替旧的值,而不像setState那样能局部更新,然后将局部更新的状态合并到整个状态之中;

const [apple, setApple] = useState({a: {a1: 1}, b: 2}); 
setApple({a: {a1: 3}}) // 此时apple的值是{a1: 3},而不会是{a: {a1: 3}, b: 2}
state = {a: {a1: 1}, b: 2}; 
setState({a: {a1: 3}}); 
{a: {a1: 3}, b: 2}

useState的demo:

function FunCom(props) { 
   const [apple, setApple] = useState('apple'); 
   useEffect(() => { document.title = `${apple}`; }); 
   return (
     <div>
       <p onClick={(e) => { setApple('banana'); console.log(apple); }}>
       函数式组件-{apple}
       </p>
     </div>
   )
}
  • useEffect:

对于react来说,当渲染后(不管是首次渲染还是重复渲染),还会存在其他一些操作:数据获取,操作dom节点,设置订阅或发布等操作;

1) 类组件:一般该副作用会放置在componentDidMount,componentDidUpdate或者componentWillUnMount;
2) 函数式组件:一般副作用放置在钩子里面执行;

语法:useEffect(callback);
其中:
1) callback是一个回调函数,它里面可以访问到最新的状态,但不能直接调用同步方式下的更新状态的函数;
2) useEffect函数在首次和之后的每次渲染后,销毁前都会调用(理解为componentDidMount,componentDidUpdate或者componentWillUnMount时刻下的调用)。
3) 该钩子是异步触发;

image.png

effect调用过程:

image.png

性能优化:useEffect(callback, [callback内部的状态或者属性])

image.png

注意:

  1. 如果是[ ],则该钩子只会在mount和unmount发生反应,在update的时候不会发生反应;
  2. 如果依赖项值不变,useEffect返回的清除函数就不会再下一次渲染中被执行,只有当依赖项的值变化,在渲染的时候,清除函数才会执行然后再执行useEffect;
  • useContext:

语法:const value = React.useContext(Context);
其中Context是指React.createContext对象,value是指该对象所传递下去给后代组件的值;

作用:不管函数值组件是不是Context.Provider组件的子孙组件,都可以获取Context组件将要透传下去的值;

注意点:

  1. 该组件可以放在Context.Provider里面,也可以放在外面,所以与Context.Provider的关系无关;
  2. 如果Provider组件的值改变,该钩子也会让该函数式组件重新渲染;
const Context = React.createContext('hello world');
<Context.Provider value="asd"/>
  <Middle/>
</Context.Provider>
<FunCom/>
function Middle() {
  return (
     <div>
       <Bottom/>
       <FunCom/>
    <div>
 )
}
function Bottom(props) {
   return <Context.Consumer>
      {
        val => {
           return <div>{value}-jiji</div>
        }
      }
   </Context.Consumer>
}
function FunCom(props) { 
   const context = useContext(Context); 
   console.log('context:', context);
   return (
      <div>
         <p>context上下文</p>
      </div>
  )
}
  • useReducer:

语法:const {state, dispatch} = useReducer(reducer, initState);
其中state就是当前的状态值,dispatch就是用来触发reducer跟新状态的方法,reducer是纯函数,initState为state的初始值;

useState和useReducer的区别:
前者状态更新是的本质是代替,更新前后的数据结构有可能不一样;而后者则是状态的合并,类似于setState,将局部状态合并到当前状态;对于简单的数据结构,可以用前者,对于复杂的数据结构,则需要用到后者;
例子:状态为{a: {a1: 1}, b: 2},先更新a1为3
image.png

demo:
image.png

  • useCallback钩子和useMemo钩子:

背景:由于函数式组件没有scu,只要父组件或者内部的状态发生变化,都会直接让函数式组件rerender,此时如果函数式组件中存在:
1) 某个值需要依赖某个状态进行比较复杂的计算和循环计算等;
2) 含有子组件,并且子组件的属性指向某个状态;
此时不管该状态有没有发生变化,都会进行重新的渲染和计算,从而造成资源的浪费;

function A() {
    const [a1, setA1] = useState(10); 
    const [a2, setA2] = useState(20);
    const value = () => { 
        let sum = 0; 
        for(let i = 0; i < a1; i++) { sum = sum + i; } 
        return sum;
    }
    return (
       <div>
         <p>{value()}</p>
         <B cb={value}/>
         <button onClick={() => { setA2(21); }}/>
       </div>
    )

作用:
useCallback和useMemo这两个钩子,主要是作用缓存作用,只要输入源的值没变,他们就会直接采用前一次计算好的缓存,减少没必要的计算和重复渲染,一旦输入源的值发生变化,他们就会重新计算,抛弃缓存;这样做可以优化了组件的性能。

钩子说明:

1.useMemo钩子:

语法:const value = useMemo(fn, input);

其中,fn就是含有计算相关的回调函数,input就是输入源,value就是fn计算出来的值;

说明:input就是fn当中的依赖项,可以是属性或者内部状态,一开始时会执行fn,并将值返回给value,当二次渲染时,此时如果input当中的依赖项的值没有发生变化,则会采用上次执行的结果返回给value,如果input的值发生变化,则会重新执行fn,得到新的value值;

使用场景:如果A组件中有个值需要依赖a状态进行复杂的计算后得到,并且A组件中的子组件B某个属性又和该值产生联系,此时可以采用useMemo,这样可以减少无畏的计算和没必要的渲染。

function WithMemo() {
   const [count, setCount] = useState(1); 
   const [val, setValue] = useState('');
   const expensive = useMemo(() => {
       console.log('compute'); 
       let sum = 0; 
       for (let i = 0; i < count * 100; i++) { sum += i; }   
       return sum;
   }, [count]);
   return <div>
     <h4>{count}-{expensive}</h4>
     {val}
     <div>
        <button onClick={() => setCount(count + 1)}>+c1</button>
        <input value={val} onChange={event => setValue(event.target.value)}/>
        <B val={expensive}/>
     </div>
   <div>
}

2.useCallback钩子:

语法:const fn2 = useCallback(fn1, input),
其中fn1是回调函数,input是输入源,fn2是返回的函数;

说明:首次渲染时,会将fn1返回给fn2,然后执行fn2获取对应的值;当二次渲染时,input的值没有发生变化,则fn2还是上一次的fn2,此时fn2虽然还是函数,但前一次和后一次都是指向同一个指针,因此是同一个函数,若input的值发生变化,此时会返回一个新的fn1给fn2,这时候的fn2就会和上一次的fn2不一样,虽然函数的内容是一样,但指向不一样。

使用场景:多数使用在当该函数作为子组件的属性函数传到子组件。

demo:

image.png

useEffect,useMemo,useCallback的区别:
useEffect(fn, input)主要是针对副作用,返回的是一个清除函数,而且没有缓存作用,只有在组件更完成新且input的值发生变化时才会触发,他只能减少没必要的副作用执行。
useMemo,useCallback主要是针对复杂的计算以及子组件的优化(配合子组件的scu),减少没必要复杂计算和子组件的渲染。

image.png

  • useRef:

语法:const curRef = useRef(initState);

说明:要区别组件的ref,因为useRef返回的对象更像一个容器,容器的形式为{current: initState};该容器可以储存很多东西,其中包括类组件(与组件的ref绑定),标签,变量等等,而且该容器会一直存在该函数式组件的整个生命周期,即只有在组件被销毁的时候才消失;

使用场合:

  1. 用来获取类组件,原生组件,标签的内部元素,只要和他们的ref绑定,就可以在函数式组件中直接访问(其实就是存储了一份目标的dom结构);
  2. 由于它的返回值存在组件整个生命周期,所以可以用来存储一些变量或者函数(例如定时器返回值等);

注意:

  1. 不能存储函数式组件。
  2. 修改curRef.current的值不会引起组件的重新渲染;
function ShowRef(props) { 
   const [a, setA] = useState(1); 
   const mount = useRef({isMount: false}); 
   const dom = useRef(null); 
   useEffect(() => { 
      if (!mount.current.isMount) { 
         console.log('again'); 
         mount.current.isMount = true; 
      } 
      console.log('Mount?', mount.current.isMount); 
      console.log('dom', dom.current);
   });
   const click = function(e) {
      setA(a + 1); 
      dom.current.style.color = 'blue'; 
  };
  return (
    <div ref={dom}>
       <div onClick={click}>ref1</div>
    </div>
  )
  • useImperativeMethods:

语法:useImperativeMethods(ref, createInstance, [input]);

说明:就是让父组件能访问函数式组件内部成分,相当于函数式组件的ref;其中ref是函数式组件传过来的参数,createInstance是给父组件访问的属性和方法实例;

使用场合:就是父组件访问子组件(函数式组件)的时候用;

demo:

function FancyInput(props, ref) {
     const inputRef = useRef(); 
     useImperativeMethods(ref, () => ({
        focus: () => { inputRef.current.focus(); } 
     })); 
     return <input ref={inputRef} ... />; 
}
FancyInput = forwardRef(FancyInput);
父组件:<FancyInput ref = {fancyInputRef} />
调用: fancyInputRef.current.focus()
  • useLayoutEffect:

语法:useLayoutEffect(fn)

作用:主要使用在dom新更后,用来访问dom节点布局方面的操作,例如宽高等;这个是用在处理DOM的时候,当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制。

以上就是我对react hook使用上的一些理解和总结,如有不对请指出。


DragonChen
285 声望15 粉丝

下一篇是:Axios源码解析。