美文网首页
useEffect和useLayoutEffect区别

useEffect和useLayoutEffect区别

作者: 木匠_说说而已 | 来源:发表于2019-06-18 19:27 被阅读0次

    官方解释

    官方解释,这两个hook基本相同,调用时机不同,请全部使用useEffect,除非遇到bug或者不可解决的问题,再考虑使用useLayoutEffect。还举了个例子,譬如你想测量DOM元素时候,使用useLayoutEffect。个人感觉举例不恰当,测试DOM我也完全可以在useEffect中测量啊。说如果需要在paint前改变DOM,更合适。

    我做过测试,譬如一个div尺寸是200 * 200,我想改成100 * 100,如果写在useEffect中,确实会造成页面抖动,写在useLayoutEffect中可以避免。

    官方解释链接

    官方解释

    redux-react-hook 中的妙用

    redux-react-hook库中有段代码使用了useLayoutEffect,用来避免组件render两次。
    这里的useIsomorphicLayoutEffect就是useLayoutEffect(因为库要区分是浏览器还是SSR,所以上面做了处理)

        // We use useLayoutEffect to render once if we have multiple useMappedState. 
        // We need to update lastStateRef synchronously after rendering component,
        // With useEffect we would have:
        // 1) dispatch action
        // 2) call subscription cb in useMappedState1, call forceUpdate
        // 3) rerender component
        // 4) call useMappedState1 and useMappedState2 code
        // 5) calc new derivedState in useMappedState2, schedule updating lastStateRef, return new state, render component
        // 6) call subscription cb in useMappedState2, check if lastStateRef !== newDerivedState, call forceUpdate, rerender.
        // 7) update lastStateRef - it's too late, we already made one unnecessary render
        useIsomorphicLayoutEffect(() => {
          lastStateRef.current = derivedState;
          memoizedMapStateRef.current = memoizedMapState;
        });
    

    看得很懵逼,讲了如果用useEffect会带来什么问题,我模拟了很久终于模拟出来作者描述的问题(意图好猜,模拟时候有个细节很难处理)

    模拟场景简化

    场景

    我有一个数据store(对redux的store),一个组件App,组件中使用了useA和useB两个自定义hook(这对应两次调用redux-react-hook的useMappedState)。

    当我一个操作,改变store时候,去调用订阅者即A和B,A和B改变会触发App重新render。这里有个问题,A和B都是订阅者,会触发两次App重新render,作者想避免,所以会在use的时候做下处理,使用useEffect的话,会出现bug,无法如愿,下面就来模拟这个过程。

    代码实现

    function App() {
      console.log('%c App render--start-->', 'color:blue')
      const a = useA();
      const b = useB();
      
      function doSomething() {
        // dispatch();
        setTimeout(dispatch, 0)
      }
      console.log('%c App render--end-->', 'color:red')
      return (
        <div>
          <p>a: {a}</p>
          <p>b: {b}</p>
          <p><button onClick={doSomething}>dispatch</button></p>
          
        </div>
      )
    }
    
    function useA() {
      console.log('---a--hook-->')
      const [trigger, setTrigger] = useState(0);
      useEffect(() => {
        console.log('--useA--useEffect-->')
        memoStore = store;
      });
      useEffect(() => {
        const fn = subsriber(() => {
          console.log('--useA--注册函数--->', memoStore, store);
          if(store !== memoStore) {
            setTrigger(Math.random())
          }
        });
        return () => unSubsriber(fn);
      }, []);
      return store;
    }
    
    function useB() {
      console.log('---b--hook-->')
      const [trigger, setTrigger] = useState(0);
      useEffect(() => {
        console.log('--useA--useEffect-->')
        memoStore = store
      });
      useEffect(() => {
        const fn = subsriber(() => {
          console.log('--useB--注册函数--->', memoStore, store);
          if(store !== memoStore) {
            setTrigger(Math.random())
          }
        });
        return () => unSubsriber(fn);
      }, []);
      return store;
    }
    

    简化的redux:

    let store = 6;
    let memoStore = 6;
    const newStore = 8;
    
    const subsriberList = new Set();
    function subsriber(fn) {
      subsriberList.add(fn);
      return fn;
    }
    function unSubsriber(fn) {
      subsriberList.delete(fn)
    }
    function dispatch() {
      memoStore = store;
      store = newStore;
      subsriberList.forEach(fn => fn())
    }
    

    这里有一个非常重要的关键点,就是App组件中的doSometing中,dispatch一定要写在setTimeout中,否则react自动帮你优化了,模拟不出来想要的场景。

    分析

    点击按钮时候,改变了store: 6 -> 8,触发了订阅者自定义hook A和B的订阅事件。按理会触发两次App render,但是我们做了优化,在useA和useB的时候,会用新状态去覆盖旧状态,然后在订阅事件中,会对比新老状态,一致的话,就不去触发自定义hook改变了,也就不会触发App render了。
    但是使用effect的话,实际执行过程是这样的:


    使用effect的代码执行流程 控制台

    可以看到,App依旧render了两次,其中主要问题就出在useEffect注册的函数在什么时候执行,从流程图中可以看到,其不是在App组件树 render结束后立即执行的(我也不知道什么时候执行,还请哪位大佬指点),js会继续执行后面的代码(B的订阅),这个时候old=new还没有执行,所以依旧触发了第二次App组件render。

    更改useEffect为useLayoutEffect

    useA

    ...
      useLayoutEffect(() => {
        console.log('--useA--useLayoutEffect-->')
        memoStore = store;
      });
    ...
    

    useB

    ...
      useLayoutEffect(() => {
        console.log('--useA--useLayoutEffect-->')
        memoStore = store
      });
    ...
    
    useLayoutEffect执行流程 控制台

    可以看见关键点是,layoutEffect队列在组件树render结束后,会立刻同步执行(个人感觉是的),所以在第一次App render结束后,old和new就相同了,在执行B订阅时候,就会根据条件,不再触发App render了。

    总结

    // 一定要加setTimeout模拟异步操作,否则实验不出来上面的流程的
      setTimeout(()=>{
        renderApp1(); // 一些会条件性触发组件重新render的代码
        exeLayoutEffectList(); // 组件树构建完毕,会同步执行useLayoutEffect中的代码
        code1(); // 一些js代码
        code2(); // 一些js代码
        // 所有代码都执行完毕后,浏览器渲染结束后,会调用useEffect中的代码
        // 或者接到下一次组件刷新(re-render)指令,会将上一次effect队列执行完毕。我根据试验猜的
        exeEffectList(); 
        renderApp2(); // 一些会条件性触发组件重新render的代码
      }, 0)
    

    主要就是effect和layoutEffect队列的执行阶段,layout会在组件树构建完毕或者刷新完毕后同步立刻执行。effect会等其他js代码执行完毕后执行(或者遇到下一次刷新任务前)

    回过头再看react关于useLayoutEffect的官方文档:

    The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.
    Prefer the standard useEffect when possible to avoid blocking visual updates.

    • 和useEffect相同,是指他们都在组件树构建完毕之后执行的
    • 但是useLayout是在DOM突变之后立即执行的,突变是指什么?是指类似组件构建完毕之后,appendChild(reactTree)这种操作吗?
    • 可以肯定的是,是在组件树构建完毕后同步执行,之后才会去执行后面的js代码
    • 使用他来读取DOM布局尺寸,我倒感觉应该是写成设定DOM布局尺寸,这样可以防抖动,同步读取DOM布局尺寸想不懂有什么用
    • useLayoutEffect队列中的任务,会在浏览器paint之前执行(可以用来防抖)
    • 尽可能使用useEffect来避免阻塞视觉更新(见上条,阻碍paint)

    吐槽

    英语太差,好多概念模模糊糊的,但是好像看过国外文章,也有吐槽react的几个概念含糊不清的。

    相关文章

      网友评论

          本文标题:useEffect和useLayoutEffect区别

          本文链接:https://www.haomeiwen.com/subject/getqqctx.html