Appearance
Fiber 数据结构与 render/commit 源码主线
如果说 React Element 解决的是“输入长什么样”,那 Fiber 解决的就是 React 如何把一棵组件树的更新工作组织起来,并一步步算到最终结果。
这篇文章重点讲:
- Fiber 节点到底是什么
- 为什么它不是“另一个虚拟 DOM”
beginWork、completeWork、commit各自负责什么- 为什么 React 会用
child / sibling / return这套结构
1. Fiber 到底是什么
Fiber 可以看成 React 内部用来表示一个更新单元和一段节点工作状态的数据结构。
把它和 React Element 的关系拉开:
| 层 | 更像什么 |
|---|---|
| React Element | 一份轻量的 UI 描述对象 |
| Fiber | 一份带有工作状态、更新信息、树关系的可执行节点 |
也就是说:
- Element 更偏“描述要渲染什么”
- Fiber 更偏“这次更新要怎么处理这个节点”
🌟 Fiber 的重点不是“再造一份树”,而是 把节点、更新、优先级、副作用和树关系统一挂到一个可调度的数据结构上。
1.1 Fiber 的完整释义和主要能力边界
如果只把 Fiber 解释成“更细粒度调度”,还是太薄。
更完整的理解应该是:
Fiber 是 React 在调和阶段使用的一种工作节点结构。它既表示当前树上的一个节点,又记录这个节点这轮更新要处理的输入、已有状态、需要执行的副作用,以及它和父子兄弟节点之间的关系。
它真正负责的是:
- 承接 React Element 转下来的节点信息
- 保存本轮更新的工作状态
- 连接树结构,方便逐步遍历
- 保存更新队列、flags、副作用等运行期信息
- 让 render 阶段和 commit 阶段可以衔接
它不负责的是:
- 不直接等于真实 DOM
- 不单独决定任务什么时候执行
- 不等于 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;
}这里最值得先认识的是这几组字段:
tag/type:当前节点是什么类型child/sibling/return:树结构关系pendingProps/memoizedProps:本轮待处理 props 和上次记忆值memoizedState/updateQueue:状态与更新队列flags:这个节点后续 commit 时要做什么alternate:当前树和工作中树之间的对应关系
3. 为什么不是传统“数组 children”结构
React 在 Fiber 上更常见的是:
childsiblingreturn
而不是简单嵌套 children 数组直接一路递归到底。
这套结构的好处主要是:
- 更适合把树变成“链表式可遍历结构”
- 更方便中断、恢复和继续处理
- 更方便在遍历时记录父子兄弟关系
Fiber 不是只为了表达树,而是为了表达“可被逐步处理的树”。
4. alternate 在解决什么问题
这是读 Fiber 时非常关键的字段。
它主要用来连接两棵对应节点:
- 当前已经提交到页面上的 Fiber 树
- 当前这轮正在计算中的 work-in-progress Fiber 树
React 更新时,并不是直接在当前树上随便乱改,而更常见的是:
- 基于旧 Fiber 创建或复用 work-in-progress Fiber
- 在新树上计算这轮更新
- 完成后再切换成最新树
这样做能让 render 阶段和 commit 阶段的职责更清楚。
4.1 alternate 的能力边界
alternate 的价值很大,但它负责的事情也很明确。
它主要负责:
- 连接 current Fiber 和 work-in-progress Fiber
- 让 React 在新旧两棵工作树之间建立一一对应关系
- 让 render 阶段可以基于旧节点复用已有信息
它不负责:
- 不直接比较差异
- 不直接决定是否 commit
- 不单独承载副作用逻辑
所以 alternate 更像“新旧工作节点之间的桥”,而不是“更新决策器”。
5. render 阶段主线:beginWork 和 completeWork
如果把 render 阶段压缩成最值得先记的两步,就是:
beginWorkcompleteWork
5.1 beginWork 在做什么
它更偏“向下”处理。
常见职责包括:
- 看当前 Fiber 类型
- 比较本轮输入有没有变化
- 决定是否可以 bailout
- 生成或复用子 Fiber
beginWork 的重点,是决定这个节点和它的子树接下来怎么继续展开。
5.2 completeWork 在做什么
它更偏“向上”收尾。
常见职责包括:
- 补齐当前 Fiber 的完成态信息
- 收集子树副作用标记
- 为宿主节点准备 commit 所需信息
可以把它看成:beginWork 像往下拆任务,completeWork 像往上收结果。
5.3 beginWork、completeWork、commit 的边界怎么分
这 3 个词如果不放在一起看,很容易全混。
beginWork
更偏“继续展开这棵子树”。
它主要负责:
- 判断当前节点该不该继续处理
- 根据
type和输入决定走哪条分支 - 生成或复用子 Fiber
它不主要负责:
- 不直接落地 DOM
- 不做最终副作用执行
completeWork
更偏“把当前节点这轮 render 的结果收口”。
它主要负责:
- 补齐完成态信息
- 向上汇总子树副作用标记
- 为 commit 做准备
它不主要负责:
- 不直接执行 effect
- 不直接操作宿主环境
commit
更偏“把 render 算好的结果真正生效”。
它主要负责:
- 执行插入、更新、删除
- 处理 ref
- 执行布局和被动副作用
它不主要负责:
- 不重新计算整棵树
- 不重跑 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);
}这段代码最值得注意的不是语法细节,而是遍历顺序:
- 先往下走到子节点
- 子节点做完后再回到父节点
- 再处理兄弟节点
这也是为什么 Fiber 会同时保留 child / sibling / return。
7. commit 阶段在做什么
render 阶段做完后,React 还不会立刻把所有结果落到 DOM。
还要进入 commit 阶段。
commit 的重点是:
- 遍历带有副作用标记的 Fiber
- 执行插入、更新、删除
- 处理 ref
- 执行布局相关和被动副作用
把 render 和 commit 拆开看:
- render 阶段更偏“计算”
- commit 阶段更偏“真正生效”
8. flags 为什么重要
Fiber 上的 flags 很值得关注。
它更像是 这个节点在 commit 阶段需要做哪些动作的标记集合。
例如概念上常见的几类动作:
- 插入
- 更新
- 删除
- ref 相关处理
- effect 相关处理
所以 commit 阶段不是重新猜要干什么,而是根据 render 阶段已经收集好的标记去执行。
8.1 flags 的边界
flags 经常会被误解成“React 的全部副作用系统”。
它本质上只是:
- 一组提交阶段动作标记
- 用来告诉 commit 当前节点需要做哪些事
它不等于:
- 具体的 DOM 实例
- 完整的调度优先级信息
- 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.js | Fiber 节点定义 |
packages/react-reconciler/src/ReactFiberBeginWork.js | render 阶段向下处理 |
packages/react-reconciler/src/ReactFiberCompleteWork.js | render 阶段向上收尾 |
packages/react-reconciler/src/ReactFiberWorkLoop.js | work loop 主循环 |
packages/react-reconciler/src/ReactFiberCommitWork.js | commit 相关逻辑 |
建议先重点看:
- Fiber 上有哪些关键字段
performUnitOfWork怎么串beginWorkcompleteUnitOfWork怎么回溯父兄节点- 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 把最终结果提交到宿主环境。