首发于前端之巅

useEffect与useLayoutEffect

React Hook让无狀态组件拥有了许多只有有狀态组件的能力,如自更新能力(setState,使用useState),访问ref(使用useRef或useImperativeMethods),访问context(使用useContext),使用更高级的setState设置(useReducer),及进行类似生命周期的阶段性方法(useEffect或useLayoutEffect)。

当然还有一些Hook,带来了一些新功能,如useCallback,这是对事件句柄进行缓存,useState的第二个返回值是dispatch,但是每次都是返回新的,使用useCallback,可以让它使用上次的函数。在虚拟DOM更新过程中,如果事件句柄相同,那么就不用每次都进行removeEventListner与addEventListner。最后就是useMemo,取得上次缓存的数据,它可以说是useCallback的另一种形式。

useState: setState
useReducer: setState
useRef: ref
useImperativeMethods: ref
useContext: context
useCallback: 可以对setState的优化
useMemo: useCallback的变形
useLayoutEffect: 类似componentDidMount/Update, componentWillUnmount
useEffect: 类似于setState(state, cb)中的cb,总是在整个更新周期的最后才执行


从上面的描述来看useEffect的时期是非常晚,可以保证页面是稳定下来再做事情。但是useEffect与useLayoutEffect与有狀态组件的生命周期钩子又有一点不一样。

useEffect(function(){
  //dosomething
  return function(){
  //dosomething     
  }
 }, inputs)

useEffect与useLayoutEffect的第一个参数是一个函数(初始函数),这函数还会返回另一个清理用的函数(清理函数,在官方文档中没有明确的文字,都注释使用了clean up的字眼,就姑且这样叫)。当某个无狀态组件要在某个阶段执行这些钩子,它会优先执行清理函数再执行初始函数。

   <script src="./react.development.js"></script>
   <script src="./react-dom.development.js"></script>
   <script type='text/javascript' src="./lib/babel.js"></script>

	<div id='root' class="root">
	</div>
	<script type='text/babel'>
        
var container = document.getElementById('root');
var {useState, useEffect, useLayoutEffect} = React;
function Example() {
    const [count, setCount] = useState(0);
    const [text, setText] = useState('');
    var a = useRef("xxx")
    useEffect(() => {
      console.log(a, 'useEffect')
       document.title = `You clicked ${count} times`;
       return () =>{
        console.log(a, 'end useEffect')
        document.title = `remove`;
       }
     });
     useLayoutEffect(() => {
      console.log(a, 'useLayoutEffect')
       document.title = `You clicked ${count} times`;
       return () =>{
        console.log(a, 'end useLayoutEffect')
        document.title += '!!!';
       }
     });
    console.log(count, '更新Example')
    return (
      <quoteblock>
        <p>You clicked {count} times</p>
        <button onClick={() => setCount(count + 1)}>
          Click me
        </button>
        <input ref={a} value={text} onChange={function(e){
          setText(e.target.value)
        }} />
        <span>{text.length}个字符</span><Child />
      </quoteblock>
    );
}

class App extends React.Component{
    state = {
      aaa: 1
    }
    onClick(){
      this.setState(function(s){
        return {
          aaa: s.aaa +1
        }
      })
    }
    componentDidMount(){
      console.log("app mount")
    }
    componentDidUpdate(){
      console.log("app update")
    }
    render(){
      return <div>{this.state.aaa < 10 ? <Example />: null}
              <h1 onClick={this.onClick.bind(this)}>{ this.state.aaa}</h1>
        </div>
    }
}

class Child extends React.Component {
    componentDidMount(){
      console.log("Child mount")
    }
    componentDidUpdate(){
      console.log("Child update")
    }
    render(){
      return <span>Child</span>
    }
}
ReactDOM.render(<App />, container)
	
    </script>

初次渲染的界面与日志

如果我们向input输入内容,就会发现它每次都会先进行 useEffect与useLayout的清理函数,再执行他们的初始函数。并且发现useEffect的函数会在最后才执行,它会晚于包含它的父函数。我们可以点击页面上的h1标签,就可以证明这一点。

点击h1会不断递增数字,到10时会销供Example这个无狀态组件与它的子组件Child。下面是数字到10时的界面与日志。

在我的迷你React框架中是这样实现这两个钩子


export function useEffect(create, inputs) {
    return dispatcher.useEffect(create, inputs, PASSIVE, 'passive', 'unpassive');
}
export function useLayoutEffect(create, inputs) {
    return dispatcher.useEffect(create, inputs, HOOK, 'layout', 'unlayout');
}
export var dispatcher = {
    //略...
    useEffect(create, inputs, EffectTag, createList, destoryList) {
        let fiber = getCurrentFiber();
        let cb = dispatcher.useCallbackOrMemo(create, inputs);
        if (fiber.effectTag % EffectTag) {
            fiber.effectTag *= EffectTag;
        }
        let updateQueue = fiber.updateQueue;
        let list = updateQueue[createList] ||  (updateQueue[createList] = []);
        updateQueue[destoryList] ||  (updateQueue[destoryList] = []);
        list.push(cb);
    },
    //略...
};

官方React的实现

export function useLayoutEffect(
  create: () => mixed,
  inputs: Array<mixed> | void | null,
): void {
  useEffectImpl(UpdateEffect, UnmountMutation | MountLayout, create, inputs);
}

export function useEffect(
  create: () => mixed,
  inputs: Array<mixed> | void | null,
): void {
  useEffectImpl(
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    create,
    inputs,
  );
}



它们就是执行时机不一样。

当目前React Hook还是实验性质,不排除会改变。目前有9种钩子,其实之前有十种,useMutationEffect前不久已经完蛋了。useMemo与useCallback很相近,但觉得useMemo的使用场合很少,不知它会不会废掉。useEffect不好用,不像useLayoutEffect那么明显可以与有狀态组件的生命周期钩子相对应。useImperativeMethods这个名字起得不好,可能以后也会调整。当然这只是我的看法。

React Hook是一个很棒的设计,它其实是将有狀态组件的更新机制(setState/forceUpdate)的内部实现进行了更广泛的应用。当它的API稳定下来我会与大家分享它们更深层次的实现。

编辑于 2018-12-25

文章被以下专栏收录