跳到主要内容

第 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 一样,completeWorkReactFiberCompleteWork.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 属性

不是首次挂载时,走 updateHostComponent1341)。它把新旧 props 里的属性挨个比:新增、删除、变化的,统统算出来。注意这里同样只算不改。算完打上 Update 标记,真正 setAttribute 要等 commit 阶段(第 18 章)。

冒泡:把"活"汇总给父节点

两类分支的最后,都调用 bubbleProperties762)。它把这棵子树里所有需要 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 动手"在源码层面的解释。

动手实验

  1. 看 createInstance:在 createInstance 那行(ReactFiberCompleteWork.js:1389)打断点,首次挂载一个 <div>。看它是不是真的调用了 document.createElement,以及 stateNode 是不是被赋成了这个 DOM。
  2. 看从下往上:在 appendAllChildren(ReactFiberCompleteWork.js:240)打断点,渲染 div > span > b 三层。观察调用顺序:先 b、再 span、最后 div,一层层往上拼。
  3. 看 flags 冒泡:在 bubbleProperties(ReactFiberCompleteWork.js:762)打断点,观察一个带子更新的子树,subtreeFlags 是怎么从小变大、最终到根。

小结

  • completeWork落实:建/更新 DOM 对象,把子 DOM 挂上来,把 flags 冒泡给父。
  • 首次挂载走 createInstance + appendAllChildren从下往上拼 DOM
  • 更新走 updateHostComponent,只算 props 差异、不打到 DOM。
  • bubblePropertiessubtreeFlags / childLanes 汇总到根,commit 阶段才知道动哪里。
  • render 阶段全程不动页面,只造对象 + 打标记。
  • 到这里,render 阶段走完了。第 17 章(commit)会消费这些标记。先看第 9 章:函数组件是怎么被调起来的。