第 8 章:completeWork——落实 DOM,向上冒泡
第 6、7 章讲的是 beginWork:往下钻,把元素变成 fiber,把子节点 diff 出来。往下钻到叶子(没有孩子的 fiber)之后,工作循环掉头往上走,这时主角换成 completeWork。
一次 render 里,每个 fiber 都经历 beginWork 和 completeWork。beginWork 管"想清楚",completeWork 管"落实"。对函数组件,落实就是"啥也不用做,把活的标记往上带";对 DOM 元素(HostComponent),落实就是真的创建一个 DOM 对象。
先记住这一句:
completeWork干三件事:建/更新对应的 DOM、把子节点的 DOM 挂上来、把整棵子树的"活"冒泡给父节点。注意它只是"造 DOM 对象 + 打标记",还没插进页面。
一图流:completeWork 的流程
按 tag 分派:两类活
和 beginWork 一样,completeWork(ReactFiberCompleteWork.js:1049)也是按 tag 分派,但活简单得多:
- 函数组件、Fragment、Memo 等:没有对应的 DOM,只有一件事,
bubbleProperties后返回 null。 - HostComponent / HostText:真正跟 DOM 打交道。
前者一行代码就完事,后者是本章主角。
首次挂载:造 DOM,从下往上拼
看 HostComponent 分支(ReactFiberCompleteWork.js:1337)。首次挂载时(current === null),它调 createInstance 创建真实 DOM 元素(1389):
const instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
createInstance 内部就是 document.createElement(type)。然后关键的 appendAllChildren(定义在 240)把子 DOM 挂上来。
注意顺序:先有子,后有父。因为遍历是从下往上回溯的,处理到父 fiber 时,它的所有子 fiber 都已经 completeWork 过了,子 DOM 都建好了。所以父可以放心地 appendChild 把它们装进来。这个"从下往上拼 DOM"的设计,正是遍历顺序决定的。
更新:diff 属性
不是首次挂载时,走 updateHostComponent(1341)。它把新旧 props 里的属性挨个比:新增、删除、变化的,统统算出来。注意这里同样只算不改。算完打上 Update 标记,真正 setAttribute 要等 commit 阶段(第 18 章)。
冒泡:把"活"汇总给父节点
两类分支的最后,都调用 bubbleProperties(762)。它把这棵子树里所有需要 commit 阶段处理的标记(subtreeFlags)和子树里还有哪些 lanes 没处理完(childLanes)汇总到当前 fiber 上。
这样一路从叶子冒泡到根,最后 HostRoot 拿到整棵树的 subtreeFlags,commit 阶段才能知道"这次要动哪些地方"。它和 diff 的 lastPlacedIndex 一个思路:信息从下往上汇总,决策只做一次。
render 阶段"不动页面"的秘密
至此第二部分可以收束了。你在 render 阶段看到的 DOM 操作,全是在做三件事:造对象(createInstance)、算差异(updateHostComponent)、打标记(flags)。一个真正的 DOM 都没插进 document。
Placement(要插入,第 7 章 diff 打的)、Update(要改属性)、Snapshot(要清理容器),这些标记躺在 subtreeFlags 里,随着 bubbleProperties 一路带到根。等到第 17 章 commit,才有人真的去 appendChild。这就是第 4 章那句"render 动脑子、commit 动手"在源码层面的解释。
动手实验
- 看 createInstance:在
createInstance那行(ReactFiberCompleteWork.js:1389)打断点,首次挂载一个<div>。看它是不是真的调用了document.createElement,以及stateNode是不是被赋成了这个 DOM。 - 看从下往上:在
appendAllChildren(ReactFiberCompleteWork.js:240)打断点,渲染div > span > b三层。观察调用顺序:先b、再span、最后div,一层层往上拼。 - 看 flags 冒泡:在
bubbleProperties(ReactFiberCompleteWork.js:762)打断点,观察一个带子更新的子树,subtreeFlags是怎么从小变大、最终到根。
小结
completeWork管落实:建/更新 DOM 对象,把子 DOM 挂上来,把 flags 冒泡给父。- 首次挂载走
createInstance+appendAllChildren,从下往上拼 DOM。 - 更新走
updateHostComponent,只算 props 差异、不打到 DOM。 bubbleProperties把subtreeFlags/childLanes汇总到根,commit 阶段才知道动哪里。- render 阶段全程不动页面,只造对象 + 打标记。
- 到这里,render 阶段走完了。第 17 章(commit)会消费这些标记。先看第 9 章:函数组件是怎么被调起来的。