Skip to main content

第 20 章:effect 的清理与执行

第 14 章讲了 effect 怎么登记、deps 怎么比较。第 17 章说了 useEffect 在 commit 后异步跑。本章把执行和清理这条线补齐:谁先跑、谁后跑、卸载时怎么办。

先记住这一句:effect 的执行顺序是先清理、后执行(先跑上一次的 destroy,再跑这次的 create);卸载时从下往上跑 destroy。useLayoutEffect 在 commit 内同步,useEffect 在 commit 后异步,但两者的"先清后建"规则一样。

一图流:effect 生命周期

先清理,后执行

一个 effect 节点上存着 create 和它返回的 destroycommitPassiveUnmountEffects 负责清理,commitPassiveMountEffects 负责执行)。每次 effect 重新执行时,顺序固定是:

  1. 先跑上一次的 destroy(清理旧副作用:清定时器、解绑事件)
  2. 再跑这次的 create(建立新副作用)

deps 变了才重跑;deps 没变(第 14 章没打 HookHasEffect)时,这次连 create 都不执行,destroy 自然也不用跑。

卸载时:从下往上清理

组件卸载(被删除)时,commitPassiveUnmountEffects 走一趟被删的子树,从子到父依次执行每个 effect 的 destroy。为什么自下而上?因为父组件的清理可能依赖子组件已经清理完的状态,反过来顺序就乱了。这也是 React 官方文档里"卸载顺序从内到外"的源码出处。

两套执行器:同步 vs 异步

  • layout 阶段commitLayoutEffectOnFiber592):跑 useLayoutEffect,在 commit 的同步路径里、paint 前。它的清理也在这个阶段先跑。
  • passive 阶段commitPassiveMountEffects / commitPassiveUnmountEffects):跑 useEffect,被调度成 commit 后的异步任务。先调度清理,再调度执行,保证同一批 effect 内部"先清后建"。

两条线共享同一套 effect 链表,区别只在时机。所以 useLayoutEffect 能同步读布局、useEffect 只能等 paint 后,这是第 14 章 flag 区分的最终落地。

动手实验

  1. 看先清后建useEffect(() => { console.log('create'); return () => console.log('destroy'); }, [count]),点几次按钮,观察每次更新日志顺序永远是 destroy 在前、create 在后。
  2. 看卸载顺序:父组件和子组件各写一个 useEffect 的 destroy,卸载父组件,观察控制台顺序是"子先、父后"。
  3. 对比两套时机:同时用 useEffectuseLayoutEffect,在 commitPassiveMountEffects(3418)打断点,确认 passive 确实在 commit 同步路径之外。

小结

  • effect 执行先清理 destroy、后执行 create,deps 没变则都不跑。
  • 卸载时自下而上跑 destroy(父依赖子已清理)。
  • 两套执行器:layout 同步画面前passive 异步 paint 后,共用同一套链表。
  • 第四部分到此收尾。下一步进入更新与优先级:第 21 章一次 setState 的完整旅程。