第 20 章:effect 的清理与执行
第 14 章讲了 effect 怎么登记、deps 怎么比较。第 17 章说了 useEffect 在 commit 后异步跑。本章把执行和清理这条线补齐:谁先跑、谁后跑、卸载时怎么办。
先记住这一句:effect 的执行顺序是先清理、后执行(先跑上一次的 destroy,再跑这次的 create);卸载时从下往上跑 destroy。
useLayoutEffect在 commit 内同步,useEffect在 commit 后异步,但两者的"先清后建"规则一样。
一图流:effect 生命周期
先清理,后执行
一个 effect 节点上存着 create 和它返回的 destroy(commitPassiveUnmountEffects 负责清理,commitPassiveMountEffects 负责执行)。每次 effect 重新执行时,顺序固定是:
- 先跑上一次的
destroy(清理旧副作用:清定时器、解绑事件) - 再跑这次的
create(建立新副作用)
deps 变了才重跑;deps 没变(第 14 章没打 HookHasEffect)时,这次连 create 都不执行,destroy 自然也不用跑。
卸载时:从下往上清理
组件卸载(被删除)时,commitPassiveUnmountEffects 走一趟被删的子树,从子到父依次执行每个 effect 的 destroy。为什么自下而上?因为父组件的清理可能依赖子组件已经清理完的状态,反过来顺序就乱了。这也是 React 官方文档里"卸载顺序从内到外"的源码出处。
两套执行器:同步 vs 异步
- layout 阶段(
commitLayoutEffectOnFiber,592):跑useLayoutEffect,在 commit 的同步路径里、paint 前。它的清理也在这个阶段先跑。 - passive 阶段(
commitPassiveMountEffects/commitPassiveUnmountEffects):跑useEffect,被调度成 commit 后的异步任务。先调度清理,再调度执行,保证同一批 effect 内部"先清后建"。
两条线共享同一套 effect 链表,区别只在时机。所以 useLayoutEffect 能同步读布局、useEffect 只能等 paint 后,这是第 14 章 flag 区分的最终落地。
动手实验
- 看先清后建:
useEffect(() => { console.log('create'); return () => console.log('destroy'); }, [count]),点几次按钮,观察每次更新日志顺序永远是 destroy 在前、create 在后。 - 看卸载顺序:父组件和子组件各写一个
useEffect的 destroy,卸载父组件,观察控制台顺序是"子先、父后"。 - 对比两套时机:同时用
useEffect和useLayoutEffect,在commitPassiveMountEffects(3418)打断点,确认 passive 确实在 commit 同步路径之外。
小结
- effect 执行先清理 destroy、后执行 create,deps 没变则都不跑。
- 卸载时自下而上跑 destroy(父依赖子已清理)。
- 两套执行器:layout 同步画面前,passive 异步 paint 后,共用同一套链表。
- 第四部分到此收尾。下一步进入更新与优先级:第 21 章一次 setState 的完整旅程。