第 17 章:commit 三阶段——before mutation → mutation → layout
第 4 章的全局地图上,render 之后是 commit。本章把 commit 放大:它把 render 算好的 finishedWork 树落成真实的 DOM 变更,全程同步、不可中断,分三个阶段依次执行。
先记住这一句:commit 拆三段,职责各不同:before mutation 在动手前读旧树,mutation 动手改 DOM,layout 改完后跑 layoutEffect。三段都跑完,
current指针才换到新树,页面才算"更新完成"。
一图流:commit 三阶段
为什么拆三段
commitRootImpl 里,三个阶段是依次同步调用的(3699、3857、3951)。源码注释给了拆分的理由(3696):
The first phase a "before mutation" phase. We use this phase to read the state of the host tree right before we mutate it.
因为改之前需要最后一次"读"旧树。最典型的例子是 getSnapshotBeforeUpdate:它要在 DOM 被改掉之前,把旧的滚动位置、DOM 状态存下来。等 mutation 一动 DOM,就再也读不到了。所以 React 把"读"和"改"硬拆成两个阶段,保证顺序。
before mutation:动手前的最后一次读
commitBeforeMutationEffects(ReactFiberCommitWork.js:340)遍历 fiber 树,执行三类"改前"工作:
- 调用
getSnapshotBeforeUpdate(Class 组件):读旧 DOM 状态,返回值传给componentDidUpdate。 - 触发
onRecoverableError之类与本次提交相关的回调。 - 处理需要"改前快照"的特殊情况。
这一段不碰 DOM,纯读纯记。
mutation:动手改 DOM
commitMutationEffects(ReactFiberCommitWork.js:1944)是真正动手的地方。它沿 subtreeFlags 找到所有要动的 fiber(第 8 章冒泡上去的标记,在这里消费),执行:
Placement:插入/移动 DOM(第 7 章 diff 打的标记)Update:改属性、改文本(第 8 章 updateHostComponent 算的差异)Deletion:删除 DOM,并执行卸载时的清理(effect 的 destroy、Class 的componentWillUnmount)
到这里,页面上的 DOM 已经变了。但副作用函数(useLayoutEffect / useEffect 的 create)还没跑。
layout:改完后跑 layoutEffect
commitLayoutEffects(ReactFiberCommitWork.js:2876)在 DOM 改完后执行:
useLayoutEffect的 create 函数(第 14 章那个Layoutflag,同步、画面前)- Class 的
componentDidMount/componentDidUpdate - ref 的赋值(把 DOM 实例挂到
ref.current)
layout 阶段跑完,浏览器才会画这帧。所以 layoutEffect 里读 DOM 布局(offsetWidth 之类)是可靠的,它保证在 paint 之前。
commit 之后:passive 异步收尾
useEffect 的执行(commitPassiveMountEffects,4550)不在 commit 的同步路径里。它被调度成 commit 之后异步跑(第 20 章详讲)。所以 useEffect 里读 DOM 布局,一定是在 paint 之后。
为什么不可中断
commit 的整个流程在 try { ... } finally { ... } 里同步执行(3694 附近),中间没有任何 shouldYield。原因第 4 章说过:DOM 改到一半停下,页面就处于半新半旧的坏状态。render 可以停下来慢慢算,commit 必须一口气改完。
动手实验
- 看三阶段顺序:分别在
commitBeforeMutationEffects(340)、commitMutationEffects(1944)、commitLayoutEffects(2876)打断点,一次更新里观察它们各只出现一次、按顺序执行,且全程没有让出。 - 看 mutation 消费标记:在
commitMutationEffects里看Placement/Update标记(第 7、8 章打上去的),确认这里才是它们"落地"的地方。 - 对比 effect 时机:同时用
useEffect和useLayoutEffect打印,再在commitPassiveMountEffects(4550)打断点,确认 passive 在 commit 之后。
小结
- commit 三阶段:before mutation 读旧树 → mutation 改 DOM → layout 跑副作用。
- 拆分原因是"改前必须能读到旧状态"(源码注释原文)。
- mutation 消费 render 阶段打的所有标记(Placement / Update / Deletion)。
- layoutEffect 同步跑在 paint 前,useEffect 异步跑在 paint 后。
- commit 全程同步不可中断,改完 current 换新树。
- 下一步:mutation 里具体怎么改 DOM?第 18 章。