Skip to main content

第 17 章:commit 三阶段——before mutation → mutation → layout

第 4 章的全局地图上,render 之后是 commit。本章把 commit 放大:它把 render 算好的 finishedWork 树落成真实的 DOM 变更,全程同步、不可中断,分三个阶段依次执行。

先记住这一句:commit 拆三段,职责各不同:before mutation 在动手前读旧树,mutation 动手改 DOM,layout 改完后跑 layoutEffect。三段都跑完,current 指针才换到新树,页面才算"更新完成"。

一图流:commit 三阶段

为什么拆三段

commitRootImpl 里,三个阶段是依次同步调用的(369938573951)。源码注释给了拆分的理由(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:动手前的最后一次读

commitBeforeMutationEffectsReactFiberCommitWork.js:340)遍历 fiber 树,执行三类"改前"工作:

  1. 调用 getSnapshotBeforeUpdate(Class 组件):读旧 DOM 状态,返回值传给 componentDidUpdate
  2. 触发 onRecoverableError 之类与本次提交相关的回调。
  3. 处理需要"改前快照"的特殊情况。

这一段不碰 DOM,纯读纯记。

mutation:动手改 DOM

commitMutationEffectsReactFiberCommitWork.js:1944)是真正动手的地方。它沿 subtreeFlags 找到所有要动的 fiber(第 8 章冒泡上去的标记,在这里消费),执行:

  • Placement:插入/移动 DOM(第 7 章 diff 打的标记)
  • Update:改属性、改文本(第 8 章 updateHostComponent 算的差异)
  • Deletion:删除 DOM,并执行卸载时的清理(effect 的 destroy、Class 的 componentWillUnmount

到这里,页面上的 DOM 已经变了。但副作用函数(useLayoutEffect / useEffect 的 create)还没跑。

layout:改完后跑 layoutEffect

commitLayoutEffectsReactFiberCommitWork.js:2876)在 DOM 改完后执行:

  • useLayoutEffect 的 create 函数(第 14 章那个 Layout flag,同步、画面前)
  • Class 的 componentDidMount / componentDidUpdate
  • ref 的赋值(把 DOM 实例挂到 ref.current

layout 阶段跑完,浏览器才会画这帧。所以 layoutEffect 里读 DOM 布局(offsetWidth 之类)是可靠的,它保证在 paint 之前。

commit 之后:passive 异步收尾

useEffect 的执行(commitPassiveMountEffects4550)不在 commit 的同步路径里。它被调度成 commit 之后异步跑(第 20 章详讲)。所以 useEffect 里读 DOM 布局,一定是在 paint 之后。

为什么不可中断

commit 的整个流程在 try { ... } finally { ... } 里同步执行(3694 附近),中间没有任何 shouldYield。原因第 4 章说过:DOM 改到一半停下,页面就处于半新半旧的坏状态。render 可以停下来慢慢算,commit 必须一口气改完。

动手实验

  1. 看三阶段顺序:分别在 commitBeforeMutationEffects(340)、commitMutationEffects(1944)、commitLayoutEffects(2876)打断点,一次更新里观察它们各只出现一次、按顺序执行,且全程没有让出。
  2. 看 mutation 消费标记:在 commitMutationEffects 里看 Placement / Update 标记(第 7、8 章打上去的),确认这里才是它们"落地"的地方。
  3. 对比 effect 时机:同时用 useEffectuseLayoutEffect 打印,再在 commitPassiveMountEffects(4550)打断点,确认 passive 在 commit 之后。

小结

  • commit 三阶段:before mutation 读旧树 → mutation 改 DOM → layout 跑副作用
  • 拆分原因是"改前必须能读到旧状态"(源码注释原文)。
  • mutation 消费 render 阶段打的所有标记(Placement / Update / Deletion)。
  • layoutEffect 同步跑在 paint 前,useEffect 异步跑在 paint 后。
  • commit 全程同步不可中断,改完 current 换新树。
  • 下一步:mutation 里具体怎么改 DOM?第 18 章。