第 5 章:首次挂载——第一棵 Fiber 树的诞生
第 4 章的全局地图,讲的是"树已经长好了,一次 setState 更新怎么走"。但第一棵树呢?它没有 current 树可复用,没有 update 队列要处理,一切从零开始。本章把这条首次挂载的旅程走一遍,它是第 4 章那套 render + commit 机制的首跑。
先记住这一句:首次挂载 = render 阶段全新建 fiber 树 + completeWork 自下而上拼 DOM + commit 把整棵树一次插进容器。diff 在首次挂载时退化成"纯新建",没有复用、移动、删除。
一图流:首次挂载的完整旅程
触发:root.render 开工
第 1 章说过 createRoot 只搭舞台。现在 root.render(<App />) 开工,走 updateContainer(ReactFiberReconciler.js:356)。这是首次渲染,没有用户交互在等,React 给它一个高优先级 lane,尽量同步一口气跑完,尽快把第一屏画出来。
prepareFreshStack:双缓冲首次用上
renderRoot 调 prepareFreshStack(ReactFiberWorkLoop.js:1932):拿第 1 章建的 HostRoot fiber 当 current,用 createWorkInProgress(ReactFiber.js:325)复制出 workInProgress。第 3 章的双缓冲,在这里第一次真正派上用场。
beginWork 往下:全是新建
从 HostRoot 开始往下钻。关键差异:此时 current 是存在的(第 1 章建的根),但 current.child === null,树是空的。所以沿途每个元素都没有旧 fiber 可对应,reconcileChildren(第 8 章会详讲 diff)退化成"纯创建":createFiberFromElement 把元素变成 fiber,逐个挂上 child / sibling(第 3 章的三指针),一路建到叶子。
completeWork 往上:拼 DOM
到叶子掉头往回走,completeWork 的 mount 分支(ReactFiberCompleteWork.js:1337):createInstance 造出真实 DOM,appendAllChildren(240)把子 DOM 挂进父节点,自下而上,一层层拼出整棵 DOM 骨架。第 9 章会把这一站展开讲。
commit:整棵树插进容器
commitRoot(ReactFiberWorkLoop.js:3416)进入第 18 章的三阶段。mutation 阶段消费 Placement 标记:把拼好的整棵 DOM 一次性插进 container,页面第一次出现内容。layout 阶段跑首次 layoutEffect。
对比:mount 与 update
| 环节 | 首次挂载 | 后续更新 |
|---|---|---|
| beginWork | current.child 为空 → 全新建 | 有旧树 → 对比复用 |
| diff | 纯 create | 复用 / 移动 / 删除 |
| completeWork | createInstance 造 DOM | updateProperties 改属性 |
| commit | 整棵插入容器 | 局部增删改 |
这张表就是第 4 章地图和本章的差别:同一套机制,第一次跑是"从无到有",之后跑是"改旧换新"。
动手实验
- 看全新建:首次
root.render时在beginWork(ReactFiberBeginWork.js:4091)打断点,观察每个子 fiber 的current都是 null(没有旧树可对应)。 - 看拼 DOM:在
appendAllChildren(ReactFiberCompleteWork.js:240)打断点,渲染div > span > b三层,看 DOM 从b到span到div一层层往上拼。 - 看整棵插入:commit 后立刻
container.innerHTML,确认页面内容是一次性出现的(对比更新的"局部改动")。
小结
- 首次挂载 = 同一套 render + commit 机制的首跑,从空树全新建。
prepareFreshStack让第 3 章的双缓冲首次用上;beginWork 全新建、completeWork 拼 DOM、commit 整树插入。- diff 首次没活干,因为压根没有旧树可比。
- 之后每一次 setState,才进入第 22 章那条"更新旅程"。
- 下一步:这套机制里"单个 fiber 怎么处理"——第 7 章 beginWork。