跳到主要内容

第 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​

环节首次挂载后续更新
beginWorkcurrent.child 为空 → 全新建有旧树 → 对比复用
diff纯 create复用 / 移动 / 删除
completeWorkcreateInstance 造 DOMupdateProperties 改属性
commit整棵插入容器局部增删改

这张表就是第 4 章地图和本章的差别:同一套机制,第一次跑是"从无到有",之后跑是"改旧换新"。

动手实验​

  1. 看全新建:首次 root.render 时在 beginWork(ReactFiberBeginWork.js:4091)打断点,观察每个子 fiber 的 current 都是 null(没有旧树可对应)。
  2. 看拼 DOM:在 appendAllChildren(ReactFiberCompleteWork.js:240)打断点,渲染 div > span > b 三层,看 DOM 从 b 到 span 到 div 一层层往上拼。
  3. 看整棵插入:commit 后立刻 container.innerHTML,确认页面内容是一次性出现的(对比更新的"局部改动")。

小结​

  • 首次挂载 = 同一套 render + commit 机制的首跑,从空树全新建。
  • prepareFreshStack 让第 3 章的双缓冲首次用上;beginWork 全新建、completeWork 拼 DOM、commit 整树插入。
  • diff 首次没活干,因为压根没有旧树可比。
  • 之后每一次 setState,才进入第 22 章那条"更新旅程"。
  • 下一步:这套机制里"单个 fiber 怎么处理"——第 7 章 beginWork。