最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 深入理解 useEffect 和 useLayoutEffect 中回调函数的执行时机

    正文概述 掘金(MoonBall)   2021-05-07   1043

    TL;DR

    useEffectuseLayoutEffect 的使用方式相同。例如,使用 useEffect 的代码如下所示:

    useEffect(
      function create() {
        // 创建副作用的回调函数
        return function destroy() {
          // 清理副作用的回调函数
        }
      },
      // 副作用对应的依赖
      [...deps]
    )
    

    那么问题来了,React 何时执行上述 create 函数和 destroy 函数呢?

    为了回答该问题,本文包括:

    1. 在虚拟 DOM 树中,两种回调函数的执行顺序。
    2. useLayoutEffect 的回调何时触发?
    3. useEffect 的回调何时触发?

    在虚拟 DOM 树中的执行顺序

    在虚拟 DOM 树结构中,useEffectuseLayoutEffect 的回调函数执行顺序是相同的。因此,只要掌握了 useEffect,也就掌握了 useLayoutEffect。例如当虚拟 DOM 结构如下图时:

    深入理解 useEffect 和 useLayoutEffect 中回调函数的执行时机

    无论使用的 Hook 是 useEffect 还是 useLayoutEffectcreate 函数的执行顺序都是:

    2-1
    2-2
    1-1
    2-3
    1-2
    

    先 destroy 再 create

    在组件状态更新后,React 将先调用所有 destroy 函数,再调用所有 create 函数。

    destroy 顺序

    destroy 分为两类:

    1. 从组件树中删除虚拟 DOM 时,引起的 destroy 函数被调用。
    2. 组件内 Hook 的依赖发生改变,引起的 destroy 函数被调用。

    删除时

    React 使用先序遍历,处理删除时的虚拟 DOM,并执行它们的 destroy 函数。

    线上 Demo 请戳这里。本节内容对应 Demo 中的 Show1。

    const useEffectFunc = React.useEffect
    // const useEffectFunc = React.useLayoutEffect;
    
    function Comp({ name, children, v }) {
      useEffectFunc(
        function create() {
          console.log("create effect", name)
          return function destroy() {
            console.log("destroy effect", name)
          }
        },
        [v]
      )
      return (
        <ul>
          <li>
            <div>{name}</div>
            {children}
          </li>
        </ul>
      )
    }
    
    function App() {
      const [showComp, setShowComp] = useState(true)
    
      return (
        <div>
          <div>
            <button onClick={() => setShowComp(v => !v)}>
              点击{showComp ? "隐藏" : "展示"}
            </button>
          </div>
          {showComp && (
            <div>
              <Comp name="1-1">
                <Comp name="2-1" />
                <Comp name="2-2" />
              </Comp>
              <Comp name="1-2">
                <Comp name="2-3" />
              </Comp>
            </div>
          )}
        </div>
      )
    }
    

    点击按钮后,所有 Comp 组件对应的虚拟 DOM 都会被删除,此时 destroy 的调用顺序为:

    1-1
    2-1
    2-2
    1-2
    2-3
    

    更新时

    React 使用后序遍历,处理更新时的虚拟 DOM,并执行它们的 destroy 回调。

    线上 Demo 请戳这里。本节内容对应 Demo 中的 Show2。

    function App() {
      const [showComp, setShowComp] = useState(true)
    
      return (
        <div>
          <div>
            <button onClick={() => setShowComp(v => !v)}>点击更新</button>
          </div>
          <div>
            <Comp name="1-1" v={showComp}>
              <Comp name="2-1" v={showComp} />
              <Comp name="2-2" v={showComp} />
            </Comp>
            <Comp name="1-2" v={showComp}>
              <Comp name="2-3" v={showComp} />
            </Comp>
          </div>
        </div>
      )
    }
    

    点击按钮后,传给 Comp 组件的 v 发生改变。因为 Comp 中 Hook 依赖于 v,所以会执行 destroy 函数。此时 destroy 的调用顺序为:

    2-1
    2-2
    1-1
    2-3
    1-2
    

    同时存在删除和更新时

    当组件的 children 既存在被删除的虚拟 DOM,也存在更新的虚拟 DOM 时,会先处理被删除的虚拟 DOM,再处理更新的虚拟 DOM。

    线上 Demo 请戳这里。本节内容对应 Demo 中的 Show3。

    function App() {
      const [showComp, setShowComp] = useState(true)
    
      return (
        <div>
          <div>
            <button onClick={() => setShowComp(v => !v)}>点击更新</button>
          </div>
          <div>
            <Comp name="1-1" v={showComp}>
              <Comp name="2-1" v={showComp} />
              <Comp name="2-2" v={showComp} key={showComp ? "1" : "0"} />
            </Comp>
            <Comp name="1-2" v={showComp}>
              <Comp name="2-3" v={showComp} />
            </Comp>
          </div>
        </div>
      )
    }
    

    点击按钮后,因为 <Comp name="2-2" key={} /> 的 key 发生改变,所以它会被删除,然后重新创建。此时 destroy 的调用顺序为:

    2-2
    2-1
    1-1
    2-3
    1-2
    

    伪代码

    destroy 回调在虚拟 DOM 树中的执行顺序伪代码如下:

    function execDestroy(node) {
      // 执行该虚拟 DOM 相关的 destroy 回调
    }
    
    function travelDestroy(node) {
      for (const deletedChild of node.deletions) {
        // 如果该虚拟 DOM 的 children 存在删除,则处理每个被删除的虚拟 DOM
        travelDeletion(deletedChild)
      }
    
      for (const child of node) {
        travelDestroy(child)
      }
    
      execDestroy(node)
    }
    
    function travelDeletion(node) {
      execDestroy(node)
      for (const child of node.children) {
        travelDeletion(child)
      }
    }
    
    // React 最初调用
    travelDestroy(root)
    

    create 顺序

    React 使用后序遍历虚拟 DOM 树的方式,执行它们的 create 回调。

    线上 Demo 请戳这里。Demo 中所有例子的 create 回调执行顺序是相同的。其结果为:

    2-1
    2-2
    1-1
    2-3
    1-2
    

    伪代码

    create 回调在虚拟 DOM 树中的执行顺序伪代码如下:

    function execCreate(node) {
      // 执行该虚拟 DOM 相关的 create 回调
    }
    
    function travelCreate(node) {
      for (const child of node) {
        travelCreate(child)
      }
    
      execCreate(node)
    }
    
    // React 最初调用
    travelCreate(root)
    

    useEffect vs useLayoutEffect

    useLayoutEffect 的回调是在提交阶段同步执行的,而 useEffect 是在提交阶段完成后的未来某时刻执行。因此,在 useEffect 的回调中更新组件状态或修改 DOM,往往会引起页面闪一下的 bug。当遇到这类问题时,应使用 useLayoutEffect 代替 useEffect,因为同步执行的代码一定在浏览器重绘之前执行。

    何时触发 useEffect 的回调函数

    上面谈到 useEffect 的回调会在未来某时刻执行,那具体是什么时候呢?

    React 源码将 useEffect 回调的处理交给了 Scheduler 进行调度。React 源码如下:

    if (
      (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
      (finishedWork.flags & PassiveMask) !== NoFlags
    ) {
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true
        scheduleCallback(NormalSchedulerPriority, () => {
          flushPassiveEffects()
          return null
        })
      }
    }
    

    因为调度的优先级是 NormalSchedulerPriority,所以该任务最快也要在下个宏任务中才会执行。根据以上代码,我们可将 useEffect 的回调称为 Passive Effect。

    但 useEffect 的回调函数不一定在下个宏任务中执行。如果在 useEffect 的回调触发之前,React 组件又进行了一次状态更新,React 会先将之前的 Passive Effect 都处理掉。

    所以 useEffect 的回调不一定在浏览器重绘之后才执行。在以下代码中,useEffect 的回调就是在重绘之前执行的,并不会造成页面闪动。线上 Demo 请戳这里。

    const useParentEffect = useLayoutEffect
    // const useParentEffect = useEffect;
    
    function Parent() {
      const [v, setV] = useState(1)
      useParentEffect(() => {
        wait(100)
        setV(2)
      }, [])
    
      return <Child />
    }
    
    function Child() {
      const [str, setStr] = useState("111")
    
      useEffect(function create() {
        wait(500)
    
        setStr("222")
      }, [])
    
      wait(500)
      return <div>{str}</div>
    }
    

    useParentEffectuseLayoutEffect 时,页面直接展示 222。

    但是当 useParentEffectuseEffect 时,或者当注释掉 useParentEffect 的代码时,页面会先展示 111 再展示 222。

    总结

    本文可总结为以下四点内容。

    一、单独就 useEffectuseLayoutEffect 而言,会先执行所有的 destroy 回调再执行所有 create 回调。

    二、在虚拟 DOM 树中,回调函数的执行顺序如下:

    1. 对于删除的虚拟 DOM,以先序遍历虚拟 DOM 树的顺序调用 destroy
    2. 对于更新的虚拟 DOM,以后序遍历虚拟 DOM 树的顺序调用 destroy
    3. 后续遍历虚拟 DOM 树的顺序调用 create

    三、useEffectuseLayoutEffect 的区别在于: useLayoutEffect 是在提交阶段同步执行,而 useEffect 是在未来某时刻执行。

    四、useEffect 回调的执行时机为以下两种情况之一:

    1. 由 React Scheduler 调度,在后续宏任务中执行。
    2. 在下一次调和阶段之前执行。

    推荐更多 React 文章

    1. React 性能优化 | 包括原理、技巧、Demo、工具使用
    2. 聊聊 useSWR,为开发提效 - 包括 useSWR 设计思想、优缺点和最佳实践
    3. React 为什么使用 Lane 技术方案
    4. React Scheduler 为什么使用 MessageChannel 实现
    5. 为什么「不变的虚拟 DOM」可以避免组件重新 Render

    原创不易,别忘了点赞鼓励哦 ❤️


    起源地下载网 » 深入理解 useEffect 和 useLayoutEffect 中回调函数的执行时机

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    一本正经的胡说八道
    回复(0)

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元