TL;DR
useEffect 和 useLayoutEffect 的使用方式相同。例如,使用 useEffect 的代码如下所示:
useEffect(
  function create() {
    // 创建副作用的回调函数
    return function destroy() {
      // 清理副作用的回调函数
    }
  },
  // 副作用对应的依赖
  [...deps]
)
那么问题来了,React 何时执行上述 create 函数和 destroy 函数呢?
为了回答该问题,本文包括:
- 在虚拟 DOM 树中,两种回调函数的执行顺序。
 - useLayoutEffect 的回调何时触发?
 - useEffect 的回调何时触发?
 
在虚拟 DOM 树中的执行顺序
在虚拟 DOM 树结构中,useEffect 和 useLayoutEffect 的回调函数执行顺序是相同的。因此,只要掌握了 useEffect,也就掌握了 useLayoutEffect。例如当虚拟 DOM 结构如下图时:

无论使用的 Hook 是 useEffect 还是 useLayoutEffect,create 函数的执行顺序都是:
2-1
2-2
1-1
2-3
1-2
先 destroy 再 create
在组件状态更新后,React 将先调用所有 destroy 函数,再调用所有 create 函数。
destroy 顺序
destroy 分为两类:
- 从组件树中删除虚拟 DOM 时,引起的 
destroy函数被调用。 - 组件内 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>
}
当 useParentEffect 是 useLayoutEffect 时,页面直接展示 222。
但是当 useParentEffect 是 useEffect 时,或者当注释掉 useParentEffect 的代码时,页面会先展示 111 再展示 222。
总结
本文可总结为以下四点内容。
一、单独就 useEffect 或 useLayoutEffect 而言,会先执行所有的 destroy 回调再执行所有 create 回调。
二、在虚拟 DOM 树中,回调函数的执行顺序如下:
- 对于删除的虚拟 DOM,以先序遍历虚拟 DOM 树的顺序调用 
destroy。 - 对于更新的虚拟 DOM,以后序遍历虚拟 DOM 树的顺序调用 
destroy。 - 以后续遍历虚拟 DOM 树的顺序调用 
create。 
三、useEffect 和 useLayoutEffect 的区别在于: useLayoutEffect 是在提交阶段同步执行,而 useEffect 是在未来某时刻执行。
四、useEffect 回调的执行时机为以下两种情况之一:
- 由 React Scheduler 调度,在后续宏任务中执行。
 - 在下一次调和阶段之前执行。
 
推荐更多 React 文章
- React 性能优化 | 包括原理、技巧、Demo、工具使用
 - 聊聊 useSWR,为开发提效 - 包括 useSWR 设计思想、优缺点和最佳实践
 - React 为什么使用 Lane 技术方案
 - React Scheduler 为什么使用 MessageChannel 实现
 - 为什么「不变的虚拟 DOM」可以避免组件重新 Render
 
原创不易,别忘了点赞鼓励哦 ❤️
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
 - 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
 
- 提示下载完但解压或打开不了?
 
- 找不到素材资源介绍文章里的示例图片?
 
- 模板不会安装或需要功能定制以及二次开发?
 
                    
    
发表评论