Skip to content

Fiber 数据结构与 render/commit 源码主线

如果说 React Element 解决的是“输入长什么样”,那 Fiber 解决的就是 React 如何把一棵组件树的更新工作组织起来,并一步步算到最终结果。

这篇文章重点讲:

  1. Fiber 节点到底是什么
  2. 为什么它不是“另一个虚拟 DOM”
  3. beginWorkcompleteWorkcommit 各自负责什么
  4. 为什么 React 会用 child / sibling / return 这套结构

1. Fiber 到底是什么

Fiber 可以看成 React 内部用来表示一个更新单元和一段节点工作状态的数据结构。

把它和 React Element 的关系拉开:

更像什么
React Element一份轻量的 UI 描述对象
Fiber一份带有工作状态、更新信息、树关系的可执行节点

也就是说:

  1. Element 更偏“描述要渲染什么”
  2. Fiber 更偏“这次更新要怎么处理这个节点”

🌟 Fiber 的重点不是“再造一份树”,而是 把节点、更新、优先级、副作用和树关系统一挂到一个可调度的数据结构上。


1.1 Fiber 的完整释义和主要能力边界

如果只把 Fiber 解释成“更细粒度调度”,还是太薄。

更完整的理解应该是:

Fiber 是 React 在调和阶段使用的一种工作节点结构。它既表示当前树上的一个节点,又记录这个节点这轮更新要处理的输入、已有状态、需要执行的副作用,以及它和父子兄弟节点之间的关系。

它真正负责的是:

  1. 承接 React Element 转下来的节点信息
  2. 保存本轮更新的工作状态
  3. 连接树结构,方便逐步遍历
  4. 保存更新队列、flags、副作用等运行期信息
  5. 让 render 阶段和 commit 阶段可以衔接

它不负责的是:

  1. 不直接等于真实 DOM
  2. 不单独决定任务什么时候执行
  3. 不等于 React Element 本身

所以把这两个概念放回原位看:

概念主要职责主要边界
React Element描述输入不保存运行期工作状态
Fiber组织节点工作与树关系不直接落地宿主环境
scheduler安排任务时机与优先级不理解组件树细节
DOM页面真实结果不负责 React 调和逻辑

这张表最好记,因为它能直接回答“Fiber 到底是不是虚拟 DOM”“Fiber 到底是不是调度器”这些高频误解。


2. Fiber 节点长什么样

下面是一份精简后的结构示意,不是 React 仓库里的逐字符原样代码。

js
function FiberNode(tag, pendingProps, key) {
  this.tag = tag;
  this.key = key;
  this.type = null;
  this.stateNode = null;

  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.memoizedState = null;
  this.updateQueue = null;

  this.flags = 0;
  this.subtreeFlags = 0;

  this.alternate = null;
}

这里最值得先认识的是这几组字段:

  1. tag / type:当前节点是什么类型
  2. child / sibling / return:树结构关系
  3. pendingProps / memoizedProps:本轮待处理 props 和上次记忆值
  4. memoizedState / updateQueue:状态与更新队列
  5. flags:这个节点后续 commit 时要做什么
  6. alternate:当前树和工作中树之间的对应关系

3. 为什么不是传统“数组 children”结构

React 在 Fiber 上更常见的是:

  1. child
  2. sibling
  3. return

而不是简单嵌套 children 数组直接一路递归到底。

这套结构的好处主要是:

  1. 更适合把树变成“链表式可遍历结构”
  2. 更方便中断、恢复和继续处理
  3. 更方便在遍历时记录父子兄弟关系

Fiber 不是只为了表达树,而是为了表达“可被逐步处理的树”。


4. alternate 在解决什么问题

这是读 Fiber 时非常关键的字段。

它主要用来连接两棵对应节点:

  1. 当前已经提交到页面上的 Fiber 树
  2. 当前这轮正在计算中的 work-in-progress Fiber 树

React 更新时,并不是直接在当前树上随便乱改,而更常见的是:

  1. 基于旧 Fiber 创建或复用 work-in-progress Fiber
  2. 在新树上计算这轮更新
  3. 完成后再切换成最新树

这样做能让 render 阶段和 commit 阶段的职责更清楚。


4.1 alternate 的能力边界

alternate 的价值很大,但它负责的事情也很明确。

它主要负责:

  1. 连接 current Fiber 和 work-in-progress Fiber
  2. 让 React 在新旧两棵工作树之间建立一一对应关系
  3. 让 render 阶段可以基于旧节点复用已有信息

它不负责:

  1. 不直接比较差异
  2. 不直接决定是否 commit
  3. 不单独承载副作用逻辑

所以 alternate 更像“新旧工作节点之间的桥”,而不是“更新决策器”。


5. render 阶段主线:beginWorkcompleteWork

如果把 render 阶段压缩成最值得先记的两步,就是:

  1. beginWork
  2. completeWork

5.1 beginWork 在做什么

它更偏“向下”处理。

常见职责包括:

  1. 看当前 Fiber 类型
  2. 比较本轮输入有没有变化
  3. 决定是否可以 bailout
  4. 生成或复用子 Fiber

beginWork 的重点,是决定这个节点和它的子树接下来怎么继续展开。

5.2 completeWork 在做什么

它更偏“向上”收尾。

常见职责包括:

  1. 补齐当前 Fiber 的完成态信息
  2. 收集子树副作用标记
  3. 为宿主节点准备 commit 所需信息

可以把它看成:beginWork 像往下拆任务,completeWork 像往上收结果。


5.3 beginWorkcompleteWorkcommit 的边界怎么分

这 3 个词如果不放在一起看,很容易全混。

beginWork

更偏“继续展开这棵子树”。

它主要负责:

  1. 判断当前节点该不该继续处理
  2. 根据 type 和输入决定走哪条分支
  3. 生成或复用子 Fiber

它不主要负责:

  1. 不直接落地 DOM
  2. 不做最终副作用执行

completeWork

更偏“把当前节点这轮 render 的结果收口”。

它主要负责:

  1. 补齐完成态信息
  2. 向上汇总子树副作用标记
  3. 为 commit 做准备

它不主要负责:

  1. 不直接执行 effect
  2. 不直接操作宿主环境

commit

更偏“把 render 算好的结果真正生效”。

它主要负责:

  1. 执行插入、更新、删除
  2. 处理 ref
  3. 执行布局和被动副作用

它不主要负责:

  1. 不重新计算整棵树
  2. 不重跑 diff 主逻辑

🌟 一句话压缩就是:beginWork 负责向下算,completeWork 负责向上收,commit 负责真正落地。`


6. 一段精简后的 work loop 示意

下面这段代码不是 React 源码原文,而是保留核心结构后的阅读辅助版本。

js
function workLoop() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork) {
  const next = beginWork(unitOfWork);
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

function completeUnitOfWork(unitOfWork) {
  let completedWork = unitOfWork;

  do {
    completeWork(completedWork);

    if (completedWork.sibling !== null) {
      workInProgress = completedWork.sibling;
      return;
    }

    completedWork = completedWork.return;
    workInProgress = completedWork;
  } while (completedWork !== null);
}

这段代码最值得注意的不是语法细节,而是遍历顺序:

  1. 先往下走到子节点
  2. 子节点做完后再回到父节点
  3. 再处理兄弟节点

这也是为什么 Fiber 会同时保留 child / sibling / return


7. commit 阶段在做什么

render 阶段做完后,React 还不会立刻把所有结果落到 DOM。

还要进入 commit 阶段。

commit 的重点是:

  1. 遍历带有副作用标记的 Fiber
  2. 执行插入、更新、删除
  3. 处理 ref
  4. 执行布局相关和被动副作用

把 render 和 commit 拆开看:

  1. render 阶段更偏“计算”
  2. commit 阶段更偏“真正生效”

8. flags 为什么重要

Fiber 上的 flags 很值得关注。

它更像是 这个节点在 commit 阶段需要做哪些动作的标记集合。

例如概念上常见的几类动作:

  1. 插入
  2. 更新
  3. 删除
  4. ref 相关处理
  5. effect 相关处理

所以 commit 阶段不是重新猜要干什么,而是根据 render 阶段已经收集好的标记去执行。


8.1 flags 的边界

flags 经常会被误解成“React 的全部副作用系统”。

它本质上只是:

  1. 一组提交阶段动作标记
  2. 用来告诉 commit 当前节点需要做哪些事

它不等于:

  1. 具体的 DOM 实例
  2. 完整的调度优先级信息
  3. Element 输入数据本身

所以 flags 负责的是“告诉 commit 做什么”,而不是“自己把事情做完”。


9. 一张图把 Fiber 主线串起来

mermaid
flowchart TD
    A[React Element] --> B[创建 / 复用 Fiber]
    B --> C[beginWork]
    C --> D[继续向下生成子 Fiber]
    D --> E[completeWork]
    E --> F[向上收集 flags]
    F --> G[commitRoot]
    G --> H[DOM 变更 / ref / effect]

10. 常见相关源码文件

如果你继续往仓库里读,最常见会遇到这些文件:

文件主要职责
packages/react-reconciler/src/ReactFiber.jsFiber 节点定义
packages/react-reconciler/src/ReactFiberBeginWork.jsrender 阶段向下处理
packages/react-reconciler/src/ReactFiberCompleteWork.jsrender 阶段向上收尾
packages/react-reconciler/src/ReactFiberWorkLoop.jswork loop 主循环
packages/react-reconciler/src/ReactFiberCommitWork.jscommit 相关逻辑

建议先重点看:

  1. Fiber 上有哪些关键字段
  2. performUnitOfWork 怎么串 beginWork
  3. completeUnitOfWork 怎么回溯父兄节点
  4. commit 为什么不和 render 混在一起

11. 这一层最常见的误区

11.1 以为 Fiber 就是虚拟 DOM 2.0

不够准确。

Fiber 的重点是更新工作的数据结构和调度模型。

11.2 以为 render 阶段已经改了 DOM

不是。

真正落地通常在 commit。

11.3 以为 child / sibling / return 只是换个写法

它更关键的价值是让树变成更适合逐步遍历和调度的结构。


12. 一句话总结

Fiber 这一层的源码主线,本质上是在做 把 React Element 转成可执行的 Fiber 树,在 render 阶段通过 beginWork 和 completeWork 逐步计算下一棵树,再在 commit 阶段根据 flags 把最终结果提交到宿主环境。

基于 VitePress 构建的个人技术笔记。