React 19 内核探秘
第3章 Fiber 架构:React 的操作系统
第3章 Fiber 架构:React 的操作系统
本章要点
- Fiber 诞生的历史背景:Stack Reconciler 的致命缺陷与浏览器渲染机制的冲突
- Fiber 数据结构的完整剖析:30+ 字段的设计意图与相互关系
- 双缓冲(Double Buffering)机制:current 树与 workInProgress 树的交替策略
- Fiber 工作循环:从
performUnitOfWork到completeWork的递归拆解- Fiber 与操作系统的类比:进程调度、中断恢复、时间片轮转
- Lane 模型:React 的优先级系统如何用位运算实现 O(1) 调度
2016 年,Andrew Clark 在 GitHub 上公开了一份名为 "React Fiber Architecture" 的文档(他在文档里特意注明:这不是官方文档,写作时他本人并不在 React 团队,后来才加入)。文档正文的第一句话是:
"React Fiber is an ongoing reimplementation of React's core algorithm. It is the culmination of over two years of research by the React team."
—— Andrew Clark, React Fiber Architecture,"Introduction" 开篇
这个看似温和的描述背后,是 React 历史上最大规模的一次内部重写。从 React 0.x 到 React 15,React 的协调算法(Reconciler)使用的是一种被称为 "Stack Reconciler" 的递归实现——它简单、直觉、高效,但有一个致命的缺陷:一旦开始渲染,就无法停止。
想象你在一台单核 CPU 的计算机上运行一个没有时间片轮转的操作系统。你启动了一个耗时 500 毫秒的计算任务。在这 500 毫秒里,键盘输入没有响应,鼠标移动被冻结,屏幕上的动画停止——因为 CPU 被那个任务完全占据,没有机会处理其他任何事情。
这就是 React 15 面临的问题。而 Fiber,就是 React 团队为它设计的"操作系统"。
3.1 Stack Reconciler:为什么需要推倒重来
递归的致命缺陷
React 15 的 Stack Reconciler 使用原生的 JavaScript 调用栈进行组件树的遍历。当你调用 setState 触发更新时,React 会从触发更新的组件开始,递归地向下遍历整个子树,对比新旧虚拟 DOM,生成变更列表,然后一次性提交到真实 DOM。
// Stack Reconciler 的简化模型(React 15)
function reconcile(parentDom: HTMLElement, oldVNode: VNode, newVNode: VNode) {
if (oldVNode.type !== newVNode.type) {
// 类型不同,直接替换
parentDom.replaceChild(createDom(newVNode), parentDom.childNodes[0]);
return;
}
// 类型相同,更新属性
updateProps(parentDom, oldVNode.props, newVNode.props);
// 递归处理 children —— 这就是"Stack"的由来
const oldChildren = oldVNode.children;
const newChildren = newVNode.children;
const maxLen = Math.max(oldChildren.length, newChildren.length);
for (let i = 0; i < maxLen; i++) {
reconcile(parentDom.childNodes[i], oldChildren[i], newChildren[i]);
// ⚠️ 每一层递归都压入调用栈
// ⚠️ 一旦开始,无法中断——JavaScript 没有"暂停调用栈"的机制
}
}
这段代码的问题在于:JavaScript 的调用栈是不可中断的。一旦你调用了 reconcile,它就会一路递归到叶子节点,然后逐层返回。在整个过程中,主线程被完全占据——没有机会处理用户输入、执行动画帧、或者响应任何其他事件。
16 毫秒的硬约束
浏览器的渲染管线以 60fps 为目标,这意味着每帧只有约 16.67 毫秒的时间窗口。在这个窗口内,浏览器需要完成:
JavaScript 执行 → 样式计算 → 布局 → 绘制 → 合成
如果 JavaScript 执行超过了这个时间窗口,浏览器就没有时间执行后续的渲染步骤——用户看到的就是"卡顿"。
sequenceDiagram
participant JS as JavaScript
participant Browser as 浏览器渲染
participant User as 用户感知
Note over JS,User: Stack Reconciler(React 15)
JS->>JS: reconcile() 开始(50ms)
Note over JS: 递归不可中断...
Note over Browser: 被阻塞,无法渲染
Note over User: 页面冻结 3 帧
JS->>Browser: reconcile() 结束
Browser->>User: 一次性更新(闪烁)
Note over JS,User: Fiber Reconciler(React 16+)
JS->>JS: 处理 5ms 的工作
JS->>Browser: 让出主线程
Browser->>User: 渲染一帧(流畅)
JS->>JS: 继续处理 5ms
JS->>Browser: 让出主线程
Browser->>User: 渲染一帧(流畅)
图 3-1:Stack Reconciler vs Fiber Reconciler 的时间线对比
让我用一个真实场景说明这个问题有多严重。
大型列表的噩梦
假设你有一个包含 10000 条记录的数据表格。用户在搜索框中输入了一个字符,触发了整个表格的重新过滤和渲染。
function DataTable({ data, filter }: { data: Item[]; filter: string }) {
const filtered = data.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
return (
<table>
<tbody>
{filtered.map(item => (
<TableRow key={item.id} item={item} />
))}
</tbody>
</table>
);
}
function SearchableTable() {
const [filter, setFilter] = useState('');
return (
<div>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="搜索..."
/>
<DataTable data={largeDataset} filter={filter} />
</div>
);
}
在 React 15 中,用户每输入一个字符,React 就会同步地渲染 10000 行表格数据。如果每行的渲染需要 0.05ms,那么整个表格的渲染就需要 500ms。在这 500ms 内:
- 用户继续输入的字符不会出现在搜索框中
- 光标停止闪烁
- 如果有动画,动画会冻结
- 整个页面处于"假死"状态
这不是一个理论上的问题——这是 2016 年无数 React 应用面临的真实困境。React 团队需要一个根本性的解决方案。
注意:Fiber 并不会自动治好这个例子。即便在 React 19 里,
onChange中的setFilter属于离散事件更新,走 SyncLane,这次渲染照样一口气做完、不可中断;要让表格渲染能被打断,得把过滤用的值交给startTransition或useDeferredValue(见第 9 章 §9.2)。
3.2 Fiber 的核心思想:把递归变成迭代
Fiber 的核心思想可以用一句话概括:把不可中断的递归,变成可中断的迭代。
在计算机科学中,任何递归都可以用显式的数据结构(通常是栈或链表)转化为迭代。Fiber 正是这个思想的应用——它用一个链表结构(Fiber Tree)替代了 JavaScript 的调用栈,使得 React 可以在两个工作单元之间"暂停"工作,将控制权交还给浏览器,然后在下一个时间窗口"恢复"工作。
这就像操作系统的上下文切换(Context Switch)——操作系统通过保存和恢复 CPU 寄存器的状态,让多个进程"轮流"使用 CPU,营造出"同时运行"的幻觉。Fiber 通过保存和恢复 Fiber 节点的状态,让 React 的渲染工作和浏览器的渲染工作"轮流"使用主线程。
// 概念模型:从递归到迭代的转变
// ❌ 递归(Stack Reconciler)—— 调用栈隐式管理状态
function processTree(node: VNode) {
processNode(node);
for (const child of node.children) {
processTree(child); // 递归调用,状态在调用栈中
}
}
// ✅ 迭代(Fiber Reconciler)—— 显式数据结构管理状态
function workLoop(deadline: IdleDeadline) {
while (workInProgress !== null && deadline.timeRemaining() > 0) {
workInProgress = performUnitOfWork(workInProgress);
// 每处理一个节点,检查是否还有时间
// 如果时间用完,workInProgress 保存了当前位置
// 下次恢复时,从这个位置继续
}
if (workInProgress !== null) {
// 还有工作没做完,请求下一个时间片
// ⚠️ 概念示意:React 16 早期曾在浏览器支持时直接用 requestIdleCallback,现行实现是自研 Scheduler
// 用 MessageChannel 驱动、每约 5ms 无条件让出,详见第 4 章 §4.1 / §4.4.2
requestIdleCallback(workLoop);
}
}
🔥 深度洞察:Fiber 不只是一个数据结构
很多教程把 Fiber 简单地描述为"一种链表数据结构"。这是不完整的。Fiber 是一个协调架构——它包含了数据结构(Fiber Node)、遍历算法(工作循环)、调度策略(优先级系统)和并发控制(中断与恢复)四个层面。把 Fiber 类比为操作系统更加贴切:Fiber Node 是进程控制块(PCB),工作循环是 CPU 调度器,Lane 是优先级队列,双缓冲是进程切换的上下文保存。理解 Fiber,就是理解 React 如何在单线程的 JavaScript 环境中模拟了一个多任务操作系统。
3.3 Fiber Node 的数据结构
每一个 React Element 在 React 内部都对应一个 Fiber Node。Fiber Node 是一个普通的 JavaScript 对象,但它的字段之丰富,足以承载 React 运行时的全部信息。
完整的 Fiber Node 定义
// packages/react-reconciler/src/ReactInternalTypes.js(v19.0.0,简化;源码是 Flow 类型 `type Fiber`)
interface FiberNode {
// === 身份标识 ===
tag: WorkTag; // Fiber 的类型标签(FunctionComponent, HostComponent, etc.)
key: string | null; // 来自 React Element 的 key
elementType: any; // 来自 React Element 的 type
type: any; // 经过解析后的 type(对于 lazy 组件,这是解析后的组件)
stateNode: any; // 对应的真实 DOM 节点(HostComponent)或组件实例(ClassComponent)
// === 树结构(链表指针)===
return: FiberNode | null; // 父节点
child: FiberNode | null; // 第一个子节点
sibling: FiberNode | null; // 下一个兄弟节点
index: number; // 在兄弟中的位置索引
// === 引用 ===
ref: React.Ref<any> | null;
refCleanup: (() => void) | null; // ref 回调返回的清理函数(React 19 新增)
// === 状态与更新 ===
pendingProps: any; // 本次更新的新 props
memoizedProps: any; // 上次渲染的 props(已提交)
memoizedState: any; // 上次渲染的 state(Hooks 链表的头节点)
updateQueue: UpdateQueue | null; // 待处理的更新队列
dependencies: Dependencies | null; // 订阅的 context 链表
mode: TypeOfMode; // ConcurrentMode / StrictMode 等模式位
// === 副作用 ===
flags: Flags; // 副作用标记(Placement, Update, Deletion, etc.)
subtreeFlags: Flags; // 子树的副作用标记(冒泡优化)
deletions: FiberNode[] | null; // 需要删除的子 Fiber
// === 调度与优先级 ===
lanes: Lanes; // 本节点上挂起的更新优先级
childLanes: Lanes; // 子树中挂起的更新优先级
// === 双缓冲 ===
alternate: FiberNode | null; // 指向另一棵树中的对应节点
// === 调试(开发模式)===
_debugOwner?: FiberNode | null;
_debugStack?: string | Error | null; // v19 用 owner stack 取代了旧的 _debugSource
_debugHookTypes?: Array<HookType> | null;
}
这个数据结构可以分成六个维度来理解:
维度一:身份标识
| 字段 | 类型 | 作用 |
|---|---|---|
tag |
WorkTag(枚举) |
标识 Fiber 的类型,决定 React 如何处理这个节点 |
key |
string | null |
Diff 算法中用于识别列表元素的唯一标识 |
elementType |
any |
React Element 原始的 type 值 |
type |
any |
解析后的 type(lazy 组件会被解析为实际组件) |
stateNode |
any |
对应的宿主实例(DOM 节点、组件实例) |
tag 字段是 React 的"类型分发器"。v19.0.0 的 packages/react-reconciler/src/ReactWorkTags.js 实测导出 28 个 WorkTag 常量(标签值域 0–29,其中 2 与 20 是历史删除后留下的空号):
// packages/react-reconciler/src/ReactWorkTags.js:42(v19.0.0)
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const HostRoot = 3; // ReactDOM.createRoot 创建的根节点
export const HostComponent = 5; // 原生 DOM 元素(div, span, etc.)
export const HostText = 6; // 文本节点
export const Fragment = 7;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const SuspenseComponent = 13;
export const OffscreenComponent = 22;
export const CacheComponent = 24;
// ... 更多类型
维度二:树结构
Fiber 树不是传统的多叉树,而是通过三个指针实现的链表树:
graph TD
A["App<br/>return: null<br/>child: Header"] --> B["Header<br/>return: App<br/>child: Logo<br/>sibling: Content"]
A --> C["Content<br/>return: App<br/>child: Article<br/>sibling: Footer"]
A --> D["Footer<br/>return: App<br/>child: null"]
B --> E["Logo<br/>return: Header<br/>child: null"]
C --> F["Article<br/>return: Content<br/>child: Title<br/>sibling: Body"]
C --> G["Body<br/>return: Content<br/>child: null"]
F --> H["Title<br/>return: Article<br/>child: null"]
style A fill:#e3f2fd,stroke:#1565c0
style B fill:#e8f5e9,stroke:#2e7d32
style C fill:#e8f5e9,stroke:#2e7d32
style D fill:#e8f5e9,stroke:#2e7d32
图 3-2:Fiber 树的链表结构
// 三指针结构
// App.child → Header
// Header.sibling → Content
// Content.sibling → Footer
// Header.return → App
// Content.return → App
// Footer.return → App
为什么不用 children: FiberNode[] 数组?因为链表结构有两个关键优势:
优势一:O(1) 的指针挂接。 已经定位到的节点,挂上或摘下只需改几个指针,不用像数组那样搬移后续元素。但别把它夸大成"重排序更快":列表 Diff 时 reconcileChildrenArray 仍要 O(n) 地扫描新旧子节点,并用 Map 按 key 查找(见第 5 章),复杂度由这一步决定。
优势二:天然支持深度优先遍历。 Fiber 的工作循环需要深度优先遍历整棵树,链表结构让这个遍历可以不使用递归完成——只需要沿着 child 向下、沿着 sibling 向右、沿着 return 向上。
// Fiber 树的深度优先遍历(无递归)
function traverseFiberTree(root: FiberNode) {
let node: FiberNode | null = root;
while (node !== null) {
// 处理当前节点
processNode(node);
// 1. 有子节点?向下
if (node.child !== null) {
node = node.child;
continue;
}
// 2. 没有子节点,尝试兄弟节点
while (node !== null) {
// 回到根节点,遍历结束
if (node === root) return;
// 有兄弟节点?向右
if (node.sibling !== null) {
node = node.sibling;
break;
}
// 没有兄弟,向上回溯
node = node.return;
}
}
}
维度三:状态与更新
// pendingProps vs memoizedProps
// pendingProps:本次渲染传入的新 props
// memoizedProps:上次渲染已提交的 props
// 两者比较可以判断 props 是否变化
// memoizedState:对于函数组件,这是 Hooks 链表的头节点
// 每个 Hook 是链表中的一个节点
type HookState = {
memoizedState: any; // Hook 的当前值
baseState: any; // 基础 state(用于并发模式的优先级跳过)
baseQueue: Update | null;
queue: UpdateQueue; // 该 Hook 的更新队列
next: HookState | null; // 指向下一个 Hook
};
💡 最佳实践:理解
memoizedState的结构解释了为什么 Hooks 不能写在条件语句中。React 通过链表的顺序来匹配每个useState/useEffect调用。如果某次渲染跳过了一个 Hook 调用,链表的顺序就会错位,导致每个 Hook 读取了错误的状态。这不是 React 的"设计缺陷",而是一个设计取舍:React 选择不给 Hook 起名字或分配 key,只按调用顺序识别它们,换来的是 API 简洁、无需额外标识;代价就是调用顺序必须每次一致。
维度四:副作用标记
// packages/react-reconciler/src/ReactFiberFlags.js:15(v19.0.0)
export const NoFlags = 0b0000000000000000000000000000;
export const Placement = 0b0000000000000000000000000010; // 新增节点
export const Update = 0b0000000000000000000000000100; // 更新节点
export const ChildDeletion = 0b0000000000000000000000010000; // 子节点需要删除
export const ContentReset = 0b0000000000000000000000100000; // 文本内容重置
export const Callback = 0b0000000000000000000001000000; // 有 callback(setState 第二参数)
export const Ref = 0b0000000000000000001000000000; // ref 需要更新
export const Snapshot = 0b0000000000000000010000000000; // getSnapshotBeforeUpdate
export const Passive = 0b0000000000000000100000000000; // useEffect
export const Visibility = 0b0000000000000010000000000000; // Offscreen 可见性
// ... 更多标记
为什么用位掩码? 因为一个 Fiber 节点可以同时拥有多种副作用。位掩码允许用单个整数存储多个标记,用位运算进行 O(1) 的查询和修改:
// 添加标记
fiber.flags |= Placement | Update; // 同时标记为"新增"和"更新"
// 检查标记
if (fiber.flags & Placement) { /* 需要新增 */ }
if (fiber.flags & Update) { /* 需要更新 */ }
// 清除标记
fiber.flags &= ~Placement; // 移除"新增"标记
// 子树标记冒泡
parent.subtreeFlags |= child.flags | child.subtreeFlags;
// 父节点知道子树中是否有副作用,可以跳过无副作用的子树
subtreeFlags 是 React 18 引入的优化。在此之前,React 使用 "Effect List"(一个链表)收集所有有副作用的节点。subtreeFlags 的优势在于:如果一个子树的 subtreeFlags 为 0,那么整个子树可以跳过,不需要遍历。 这在大型组件树中是一个重大优化。
维度五:调度与优先级(Lanes)
// packages/react-reconciler/src/ReactFiberLane.js:18(v19.0.0;非 .ts)
export type Lane = number;
export type Lanes = number;
export const NoLane: Lane = 0b0000000000000000000000000000000;
export const SyncLane: Lane = 0b0000000000000000000000000000010;
export const InputContinuousLane: Lane = 0b0000000000000000000000000001000;
export const DefaultLane: Lane = 0b0000000000000000000000000100000;
const TransitionLane1: Lane = 0b0000000000000000000000010000000; // 模块内部常量,未导出
const TransitionLane2: Lane = 0b0000000000000000000000100000000;
// ... 共 15 条 TransitionLane(TransitionLane1 ~ TransitionLane15)
export const IdleLane: Lane = 0b0010000000000000000000000000000;
export const OffscreenLane: Lane = 0b0100000000000000000000000000000;
export const DeferredLane: Lane = 0b1000000000000000000000000000000;
Lane 模型替代的是 React 16.x 的 expirationTime 模型。这次重构(PR #18796)于 2020 年 5 月合入,随 React 17.0 发布——也就是说 React 17 内部已经是 Lane 模型,React 18 在其上继续扩展(TransitionLanes 拆成 16 条独立车道、新增 entangleTransitions 等)。它用 31 位整数的每一位表示一个优先级"车道"(Lane),优先级从低位到高位递减。
⚠️ 上面的位值取自 React 19.0 的
ReactFiberLane.js(最低位是SyncHydrationLane = 0b1,SyncLane = 0b10;OffscreenLane = 0b010…,DeferredLane = 0b100…,二者不要写反)。React 18.2 的布局不同——SyncLane = 0b1、InputContinuousLane = 0b100、DefaultLane = 0b10000。第 4 章图 4-2 同样按 v19.0.0 绘制(Sync → microtask、Discrete/Continuous → UserBlocking),不要把 18 的SyncLane = 0b1套到 19 的图上。
| Lane | 优先级 | 触发场景 | 示例 |
|---|---|---|---|
SyncLane |
最高 | 离散事件 | onClick、onKeyDown |
InputContinuousLane |
高 | 连续事件 | onMouseMove、onScroll |
DefaultLane |
中 | 普通更新 | setTimeout、fetch 回调 |
TransitionLanes |
低 | Transition 更新 | startTransition 包裹的更新 |
IdleLane |
最低 | 空闲更新 | 预加载、后台同步 |
为什么从 expirationTime 迁移到 Lanes?
expirationTime 模型用一个时间戳表示优先级——时间越早到期,优先级越高。这个模型有两个缺陷:
-
无法表示一组不连续的优先级。 如果你想处理"优先级 3 和优先级 7,但跳过优先级 5",expirationTime 做不到。Lanes 可以——
lane3 | lane7表示同时处理两个优先级。 -
无法表示一批更新。 Suspense 需要将一组 Transition 更新作为整体处理。expirationTime 只能表示一个时间点,Lanes 可以表示一组车道
TransitionLane1 | TransitionLane2 | ...。
// Lane 的位运算操作
function mergeLanes(a: Lanes, b: Lanes): Lanes {
return a | b; // 合并两组优先级
}
function intersectLanes(a: Lanes, b: Lanes): Lanes {
return a & b; // 取交集
}
function isSubsetOfLanes(set: Lanes, subset: Lanes): boolean {
return (set & subset) === subset; // 检查 subset 是否是 set 的子集
}
function getHighestPriorityLane(lanes: Lanes): Lane {
return lanes & -lanes; // 取最低位的 1,即最高优先级
// 这利用了补码的性质:-lanes = ~lanes + 1
// 例:lanes = 0b1010,-lanes = 0b0110,lanes & -lanes = 0b0010
}
🔥 深度洞察:Lanes 的位运算之美
lanes & -lanes取最低位的 1——这个技巧来自底层系统编程。在补码表示法中,-n等于~n + 1,将n的最低位 1 之上的所有位取反。与原数做与运算后,只保留了最低位的 1。这使得"获取最高优先级"成为一个 O(1) 操作,而不需要遍历所有位。React 团队选择位运算而非数组/Map 来实现优先级系统,正是因为它能在常数时间内完成所有调度决策——这对于每秒可能触发数十次更新的 UI 框架来说至关重要。
维度六:双缓冲
// alternate 字段:指向另一棵树中的对应节点
fiber.alternate = otherFiber;
otherFiber.alternate = fiber;
alternate 是 Fiber 双缓冲机制的核心字段。我们在 3.4 节详细剖析。
3.4 双缓冲机制:屏幕背后的两棵树
什么是双缓冲
双缓冲(Double Buffering)是图形编程中的经典技术。当你在屏幕上直接绘制图形时,如果绘制过程较慢,用户会看到图形被逐步绘制的过程——闪烁和撕裂。解决方案是:在一个看不见的"后台缓冲区"中完成所有绘制工作,然后一次性将整个画面复制到屏幕上。
React 的双缓冲思想相同:维护两棵 Fiber 树——current 树(当前屏幕上的状态)和 workInProgress 树(正在构建的新状态)。所有的计算工作在 workInProgress 树上进行,完成后一次性切换为 current 树。
graph LR
subgraph "Current Tree(屏幕上的状态)"
CA["App"] --> CB["Header"]
CA --> CC["Content"]
CA --> CD["Footer"]
end
subgraph "WorkInProgress Tree(正在构建的状态)"
WA["App'"] --> WB["Header'"]
WA --> WC["Content'"]
WA --> WD["Footer'"]
end
CA <-.->|alternate| WA
CB <-.->|alternate| WB
CC <-.->|alternate| WC
CD <-.->|alternate| WD
style CA fill:#e8f5e9,stroke:#2e7d32
style CB fill:#e8f5e9,stroke:#2e7d32
style CC fill:#e8f5e9,stroke:#2e7d32
style CD fill:#e8f5e9,stroke:#2e7d32
style WA fill:#fff3e0,stroke:#ef6c00
style WB fill:#fff3e0,stroke:#ef6c00
style WC fill:#fff3e0,stroke:#ef6c00
style WD fill:#fff3e0,stroke:#ef6c00
图 3-3:Fiber 的双缓冲机制
双缓冲的创建与切换
// packages/react-reconciler/src/ReactFiber.js:329(v19.0.0,简化)
// 创建 workInProgress 节点
function createWorkInProgress(current: FiberNode, pendingProps: any): FiberNode {
let workInProgress = current.alternate;
if (workInProgress === null) {
// 尚无 alternate(该节点第一次参与更新):创建新的 Fiber 节点
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
// 建立双向链接
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// 更新:复用已有的 Fiber 节点
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
// 重置副作用标记
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
}
// 只保留静态 flags(RefStatic / LayoutStatic / PassiveStatic 等)
workInProgress.flags = current.flags & StaticMask;
// 复制不变的字段
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
return workInProgress;
}
关键点:节点复用。 当 current.alternate 不为 null 时,React 不创建新对象,而是复用旧的 Fiber 节点——只更新它的属性。注意时间线:首次渲染只构建 current 树(除 HostRootFiber 外,所有节点的 alternate 都是 null);第一次更新时,createWorkInProgress 会为每个参与渲染的节点新建一次 alternate——整棵 workInProgress 树都是新对象;从第二次更新开始,两棵树才进入完全轮换复用的稳态,此后不再产生新的 Fiber Node 对象(除非有新增节点),极大地减少了 GC 压力。
Commit 阶段的切换
当 workInProgress 树构建完成后,React 进入 Commit 阶段,将变更应用到真实 DOM。Commit 完成后,只需要一行代码完成树的切换:
// packages/react-reconciler/src/ReactFiberWorkLoop.js:3213(v19.0.0,简化)
function commitRootImpl(root: FiberRoot, /* ... */) {
// ... 应用所有 DOM 变更 ...
// 🎯 关键一行:切换 current 指针
root.current = finishedWork;
// finishedWork 就是 workInProgress 树的根节点
// 现在它变成了 current 树
// 而之前的 current 树通过 alternate 指针,成为了下一次更新的 workInProgress 树
}
这行代码完成后:
- 之前的 workInProgress 树变成了新的 current 树(屏幕上显示的状态)
- 之前的 current 树通过
alternate指针,成为了下一次更新时的 workInProgress 树(被复用)
这就是双缓冲的精髓——两棵树在每次更新后交换角色,节点反复复用,不必每次重新创建。
3.5 Fiber 工作循环:深度优先的可中断遍历
Fiber 的工作循环(Work Loop)是整个架构的核心执行引擎。它将一次完整的树遍历分解为一个个"工作单元"(Unit of Work),每个工作单元处理一个 Fiber 节点。
两阶段:Render 与 Commit
React 的更新过程分为两个阶段:
| 阶段 | 是否可中断 | 做什么 | 涉及真实 DOM |
|---|---|---|---|
| Render 阶段 | ✅ 可中断(仅并发渲染) | 构建 workInProgress 树,计算 diff,标记副作用 | ❌ 不改动页面 DOM(新节点只在内存中创建) |
| Commit 阶段 | ❌ 不可中断 | 将副作用应用到真实 DOM,执行生命周期 | ✅ 涉及 |
Render 阶段可中断(并发渲染下)是 Fiber 的核心价值所在。Commit 阶段不可中断,因为 DOM 操作必须保持原子性——用户不应该看到"更新了一半"的界面。
performUnitOfWork:向下递
// packages/react-reconciler/src/ReactFiberWorkLoop.js:2517(v19.0.0,简化)
function performUnitOfWork(unitOfWork: FiberNode): void {
const current = unitOfWork.alternate;
// "递"阶段:处理当前节点,返回子节点
// (v19 传入的是 entangledRenderLanes,即本次 render 连带纠缠的 lanes)
const next = beginWork(current, unitOfWork, entangledRenderLanes);
// 将 pendingProps 提升为 memoizedProps(本次处理完毕)
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 没有子节点了,进入"归"阶段
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next; // 子节点成为下一个工作单元
}
}
beginWork:根据类型分发处理
beginWork 是 Render 阶段最重要的函数——它根据 Fiber 的 tag 决定如何处理这个节点:
// packages/react-reconciler/src/ReactFiberBeginWork.js:3824(v19.0.0,简化)
function beginWork(
current: FiberNode | null,
workInProgress: FiberNode,
renderLanes: Lanes
): FiberNode | null {
// 优化:如果 props 和 state 都没变,可以跳过
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps === newProps &&
!hasLegacyContextChanged() &&
!checkScheduledUpdateOrContext(current, renderLanes)
) {
// Bailout:跳过这个节点(子树没有挂起的更新时,整棵子树一并跳过)
// (内部会走到 bailoutOnAlreadyFinishedWork)
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
}
}
workInProgress.lanes = NoLanes;
// 根据 tag 分发处理
switch (workInProgress.tag) {
case FunctionComponent:
return updateFunctionComponent(current, workInProgress, renderLanes);
case ClassComponent:
return updateClassComponent(current, workInProgress, renderLanes);
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case MemoComponent:
return updateMemoComponent(current, workInProgress, renderLanes);
// ... 更多类型
}
}
Bailout 优化是性能的关键。 如果一个节点的 props 引用没有变化(oldProps === newProps),且没有挂起的更新,React 就不再调用这个组件、不 diff 它的 children;若子树里也没有挂起的更新(childLanes 不含本次 lanes),整个子树都会被跳过。这就是为什么 React.memo 和 React Compiler 的自动记忆化如此重要——它们让子树更频繁地命中 bailout。
completeUnitOfWork:向上归
当 beginWork 返回 null(没有子节点了),进入"归"阶段:
// packages/react-reconciler/src/ReactFiberWorkLoop.js:2814(v19.0.0,简化)
function completeUnitOfWork(unitOfWork: FiberNode): void {
let completedWork: FiberNode = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// "归"阶段的核心:创建或更新 DOM 节点
// 副作用标记的冒泡(subtreeFlags |= child.flags | child.subtreeFlags)
// 在 completeWork 末尾的 bubbleProperties(completedWork) 里完成,
// 而不是在这里由父节点收集
completeWork(current, completedWork, entangledRenderLanes);
// 有兄弟节点?返回兄弟,继续"递"
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return; // 退出 complete 循环,回到 performUnitOfWork
}
// 没有兄弟,继续向上"归"
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}
completeWork:创建真实 DOM
completeWork 在"归"阶段为 HostComponent(原生 DOM 元素)创建真实 DOM 节点:
// packages/react-reconciler/src/ReactFiberCompleteWork.js:938(v19.0.0,简化)
function completeWork(
current: FiberNode | null,
workInProgress: FiberNode,
renderLanes: Lanes
) {
switch (workInProgress.tag) {
case HostComponent: {
const type = workInProgress.type; // 'div', 'span', etc.
if (current !== null && workInProgress.stateNode !== null) {
// 更新路径:React 19 在 completeWork 只打 Update flag,
// 属性 diff + DOM 写入推迟到 commit 的 commitUpdate / updateProperties。
// (React ≤18 会在此处 prepareUpdate 生成 updatePayload,见下方说明)
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps !== newProps) {
workInProgress.flags |= Update;
}
} else {
// 首次渲染:创建 DOM 节点
const instance = createInstance(type, workInProgress.pendingProps);
// 将所有子 DOM 节点挂载到这个节点下
appendAllChildren(instance, workInProgress);
workInProgress.stateNode = instance;
}
// 把子树的 flags / lanes 冒泡到本节点(subtreeFlags、childLanes)
bubbleProperties(workInProgress);
return null;
}
case HostText: {
const newText = workInProgress.pendingProps;
if (current !== null && workInProgress.stateNode !== null) {
// 更新文本
if (current.memoizedProps !== newText) {
workInProgress.flags |= Update;
}
} else {
// 创建文本节点
workInProgress.stateNode = createTextInstance(newText);
}
bubbleProperties(workInProgress);
return null;
}
// ... 其他类型
}
}
版本对照(React ≤18):历史上
completeWork更新路径会调用prepareUpdate,在 render 阶段生成updatePayload扁平数组再挂到 fiber 上,commit 阶段按清单写 DOM。React 19 起updatePayload中间结构已移除:completeWork只打 Update flag,commit 里commitUpdate(domElement, type, oldProps, newProps)+updateProperties一次完成 diff 与写入(详见第 15 章)。下文「差异三」会再给一处对照伪代码。
完整的遍历流程
让我用一个具体的组件树来展示完整的"递-归"遍历流程:
function App() {
return (
<div>
<h1>Title</h1>
<p>Content</p>
</div>
);
}
对应的 Fiber 树遍历顺序:
beginWork(App) → 返回 child: div
beginWork(div) → 返回 child: h1
beginWork(h1) → 返回 child: "Title"
beginWork("Title") → 返回 null(叶子节点)
completeWork("Title")
completeWork(h1) ← 创建 <h1> DOM,挂载 "Title"
beginWork(p) → sibling,返回 child: "Content"
beginWork("Content") → 返回 null
completeWork("Content")
completeWork(p) ← 创建 <p> DOM,挂载 "Content"
completeWork(div) ← 创建 <div> DOM,挂载 <h1> 和 <p>
completeWork(App)
关键观察:DOM 节点是在"归"阶段自下而上创建的。 叶子节点的 DOM 先创建,然后逐层向上挂载到父节点。当 completeWork(div) 执行时,<h1> 和 <p> 的 DOM 已经准备好了,可以直接 appendChild。
3.6 可中断渲染:时间切片的实现
workLoopConcurrent:带时间检查的工作循环
// packages/react-reconciler/src/ReactFiberWorkLoop.js:2217(v19.0.0,简化)
// 同步渲染(SyncLane / InputContinuousLane / DefaultLane 等阻塞车道):不可中断
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// 并发渲染(Transition / Retry / Idle 等非阻塞车道):可中断
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
唯一的区别就是 shouldYield() 这个检查。shouldYield 来自 React 的调度器(Scheduler),它检查当前时间片是否已用完:
// packages/scheduler/src/forks/Scheduler.js:458(v19.0.0)
function shouldYieldToHost(): boolean {
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
// 主线程被占用的时间还不到一个切片,先不让出
return false;
}
return true;
// startTime 在 performWorkUntilDeadline 入口写入
// frameInterval 默认取 frameYieldMs = 5(ms)
}
5 毫秒的时间片。 React 的默认时间片是 5ms(packages/scheduler/src/SchedulerFeatureFlags.js 的 frameYieldMs = 5)——这意味着每处理一个 Fiber 节点后,React 都会检查是否已经工作了 5ms。如果是,就暂停工作,将控制权交还给浏览器,让浏览器有机会处理用户输入和渲染。
sequenceDiagram
participant Scheduler as 调度器
participant WorkLoop as 工作循环
participant Browser as 浏览器
Scheduler->>WorkLoop: 开始时间片 (5ms)
WorkLoop->>WorkLoop: performUnitOfWork(A)
WorkLoop->>WorkLoop: performUnitOfWork(B)
WorkLoop->>WorkLoop: performUnitOfWork(C)
Note over WorkLoop: shouldYield() = true
WorkLoop->>Scheduler: 时间片用完,暂停
Scheduler->>Browser: 让出主线程
Browser->>Browser: 处理用户输入
Browser->>Browser: 渲染一帧
Browser->>Scheduler: 主线程空闲
Scheduler->>WorkLoop: 继续 (从 D 开始)
WorkLoop->>WorkLoop: performUnitOfWork(D)
WorkLoop->>WorkLoop: performUnitOfWork(E)
Note over WorkLoop: 所有工作完成
WorkLoop->>Scheduler: Render 阶段完毕
图 3-4:时间切片的执行流程
中断与恢复的秘密
为什么 Fiber 可以在任意节点暂停并恢复?因为所有的状态都保存在 Fiber Node 中,而不是 JavaScript 的调用栈中。
在 Stack Reconciler 中:
reconcile(App)
→ reconcile(div) // 调用栈帧 1
→ reconcile(h1) // 调用栈帧 2
→ reconcile(text) // 调用栈帧 3
// 如果在这里中断,调用栈帧 1、2、3 的信息全部丢失
在 Fiber Reconciler 中:
workInProgress = h1Fiber // 当前工作到 h1
// 中断后,只需要恢复 workInProgress 指针
// h1Fiber.return → div,div.return → App,所有信息都在链表中
这就是为什么 Fiber 需要那么多字段——每个字段都是"上下文"的一部分,确保中断后可以无损恢复。
3.7 Fiber 与操作系统的完整类比
理解 Fiber 最好的方式,是将它与操作系统进行系统性的类比:
| 操作系统概念 | Fiber 对应物 | 说明 |
|---|---|---|
| 进程/线程 | Fiber Node | 代表一个可调度的工作单元 |
| 进程控制块(PCB) | Fiber Node 的字段 | 保存状态、优先级、上下文信息 |
| 上下文切换 | 中断 workLoop + 保存 workInProgress | 暂停当前工作,切换到其他任务 |
| 时间片轮转 | 5ms 时间片 + shouldYield | 每个工作单元执行一段时间后让出 CPU |
| 优先级调度 | Lane 模型 | 高优先级更新(用户输入)可以打断低优先级更新(如 Transition 更新) |
| 抢占式调度 | 高优先级 Lane 打断低优先级渲染 | 类似于 Unix 的信号中断 |
| 双缓冲帧 | current 树 / workInProgress 树 | 后台计算,一次性切换到前台 |
| 虚拟内存/页表 | Fiber 树的链表结构 | 通过指针间接访问,支持动态调整 |
| 内核态/用户态 | Render 阶段 / Commit 阶段 | Render 可中断(用户态),Commit 不可中断(内核态) |
🔥 深度洞察:React 是一个运行在浏览器中的操作系统
这不只是一个隐喻——Fiber 本身就是对调用栈的重新实现。Andrew Clark 在 "React Fiber Architecture" 文档里写道:"You can think of a single fiber as a virtual stack frame."——Fiber 就是 React 为组件自己实现的、可以被调度器随时保存与恢复的"虚拟栈帧"。理解这一点,你就能理解 React 的很多设计决策:为什么需要自己的调度器(不依赖
requestIdleCallback)、为什么 Hooks 有那么多规则(类似于系统调用的约束)、为什么并发模式如此复杂(并发编程本来就复杂)。React 不只是一个 UI 库——它是一个 UI 调度引擎。
3.8 首次渲染(Mount)的完整流程
让我们用一个真实的例子,追踪 Fiber 在首次渲染时的完整行为。
// 入口代码
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<App />);
function App() {
const [count, setCount] = useState(0);
return (
<div className="app">
<h1>Count: {count}</h1>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
步骤一:创建 FiberRoot 和 HostRootFiber
// ReactDOM.createRoot 内部
function createRoot(container: Element): Root {
// FiberRoot:整个应用的根,不是 Fiber 节点
const fiberRoot: FiberRoot = {
containerInfo: container, // 真实 DOM 容器
current: null, // 下面再指向 HostRootFiber
finishedWork: null, // commit 阶段的入口
pendingLanes: NoLanes,
// ...
};
// HostRootFiber:Fiber 树的根节点
const hostRootFiber: FiberNode = {
tag: HostRoot,
stateNode: fiberRoot, // 反向指向 FiberRoot
// ...
};
fiberRoot.current = hostRootFiber;
return fiberRoot;
}
FiberRoot 和 HostRootFiber 是两个不同的东西:FiberRoot 是容器,HostRootFiber 是 Fiber 树的根。它们通过 current 和 stateNode 互相指向。
步骤二:调度更新
// root.render(<App />) 内部
// packages/react-reconciler/src/ReactFiberReconciler.js(v19.0.0,简化:
// updateContainer 算出 lane 后交给 updateContainerImpl)
function updateContainer(element: ReactElement, container: FiberRoot): Lane {
const hostRootFiber = container.current;
// 根据当前上下文计算优先级
const lane = requestUpdateLane(hostRootFiber);
// render() 在事件回调之外调用,取 DefaultEventPriority → DefaultLane
// 创建一个更新对象
const update = createUpdate(lane); // 首次渲染拿到 DefaultLane
update.payload = { element }; // element 就是 <App />
// 将更新放入 HostRootFiber 的更新队列,返回所属的 FiberRoot
const root = enqueueUpdate(hostRootFiber, update, lane);
// 调度渲染
if (root !== null) {
scheduleUpdateOnFiber(root, hostRootFiber, lane);
entangleTransitions(root, hostRootFiber, lane);
}
return lane;
}
注意:createRoot().render() 的首次渲染不是同步优先级。它在任何事件之外调用,requestUpdateLane 取到 DefaultEventPriority,对应 DefaultLane,最终通过 Scheduler 的 NormalPriority 异步调度。"首渲走 SyncLane"是 legacy ReactDOM.render 的行为。
步骤三:Render 阶段
1. beginWork(HostRootFiber)
- 从 updateQueue 中取出 <App /> element
- 为 App 创建 FiberNode(tag: FunctionComponent)
- 返回 App Fiber 作为 child
2. beginWork(App Fiber)
- 调用 App 函数组件
- useState(0) 初始化 Hook 链表,第一个 hook 的 memoizedState = 0
- 组件返回 JSX → React Element 树
- 为 <div> 创建 FiberNode(tag: HostComponent)
- 返回 div Fiber 作为 child
3. beginWork(div Fiber)
- 读取 props.children(className 等属性要到 completeWork 创建 DOM 时才写入)
- 为 <h1> 创建 FiberNode
- 返回 h1 Fiber 作为 child
4. beginWork(h1 Fiber) → beginWork("Count: " Text)
- h1 的 children 是 ["Count: ", 0],生成 "Count: " 和 "0" 两个文本 Fiber
- 叶子节点,返回 null
5. completeWork("Count: " Text) → 兄弟 "0" 再走 beginWork / completeWork → 各创建一个文本节点
6. completeWork(h1 Fiber) → 创建 <h1> DOM,挂载两个文本节点
7. beginWork(button Fiber) → beginWork("+1" Text)
8. completeWork("+1" Text) → 创建文本节点
9. completeWork(button Fiber) → 创建 <button> DOM,挂载文本
(onClick 只是存进 props——React 17+ 事件统一委托在 root 容器上,
这里不会对 button 做 addEventListener)
10. completeWork(div Fiber) → 创建 <div> DOM,挂载 <h1> 和 <button>
11. completeWork(App Fiber) → 无 DOM 操作(函数组件没有自己的 DOM)
12. completeWork(HostRootFiber) → 准备进入 Commit 阶段
步骤四:Commit 阶段
Commit 阶段将构建好的 DOM 树一次性挂载到容器中:
// 简化的 Commit 流程
function commitRoot(root: FiberRoot) {
const finishedWork = root.finishedWork; // workInProgress 树的根
// 阶段一:Before Mutation(DOM 操作之前)
// - 调用 getSnapshotBeforeUpdate(类组件)
// 阶段二:Mutation(DOM 操作)
commitMutationEffects(finishedWork);
// - Placement:将 DOM 树挂载到 container
// - Update:更新已有 DOM 的属性
// - Deletion:移除 DOM 节点
// 🎯 切换 current 指针
root.current = finishedWork;
// 阶段三:Layout(DOM 操作之后)
commitLayoutEffects(finishedWork);
// - 调用 useLayoutEffect 的回调
// - 调用类组件的 componentDidMount/componentDidUpdate
// 阶段四:Passive Effects(默认异步执行)
scheduleCallback(flushPassiveEffects);
// - 调用 useEffect 的回调(异步,不阻塞渲染)
// - 例外:含 SyncLane 的更新会在 commit 收尾同步 flush(见第 6 章)
}
3.9 更新(Update)的完整流程
当用户点击按钮触发 setCount(c => c + 1) 时,更新流程与首次渲染有几个关键差异:
差异一:复用而非创建
// 创建 workInProgress 时复用 alternate
const workInProgress = createWorkInProgress(current, pendingProps);
// 如果 current.alternate 存在,直接复用
// 不需要创建新的 Fiber 对象
差异二:Bailout 优化
// beginWork 中的 bailout 检查(v19:checkScheduledUpdateOrContext)
if (oldProps === newProps && !checkScheduledUpdateOrContext(current, renderLanes)) {
// 这个节点不需要重新渲染(子树也无挂起更新时整棵跳过)
// attemptEarlyBailoutIfNoScheduledUpdate → bailoutOnAlreadyFinishedWork
return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
}
在上面的例子中,App 每次渲染都会通过 JSX 为 <button> 新建一个 props 对象,所以即使用 useCallback 缓存 onClick,oldProps === newProps 也不成立,button Fiber 不会命中这里的 bailout。只有整个元素被复用(例如用 useMemo 缓存这段 JSX,或由 React Compiler 自动缓存)时,button 及其子树才能被跳过。
差异三:DOM 更新而非创建
React 19 的更新路径:completeWork 发现 props 引用变化就打 Update flag;真正的属性 diff 与 DOM 写入发生在 Commit 阶段。
// React 19:completeWork 更新路径(只打标记)
if (current !== null && workInProgress.stateNode !== null) {
// 已有 DOM 节点,不重建;oldProps !== newProps 时标记 Update
if (oldProps !== newProps) {
workInProgress.flags |= Update;
}
// commit 阶段:commitUpdate(domElement, type, oldProps, newProps)
// → updateProperties 一次完成 diff + 写入
}
对比 React ≤18 / 过渡期(教学对照,非 React 19 现行路径):render 阶段 prepareUpdate 生成 updatePayload(形如 [propKey, propValue, ...] 的扁平数组),commit 再按清单写 DOM。updatePayload 中间结构在 React 19 中已移除,详见第 15 章。
3.10 Fiber 的错误处理:Error Boundary 的实现
Fiber 架构还为错误处理提供了优雅的解决方案——Error Boundary(错误边界)。
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong: {this.state.error?.message}</h1>;
}
return this.props.children;
}
}
在 Fiber 架构中,错误处理的实现依赖于 return 指针:
// packages/react-reconciler/src/ReactFiberThrow.js:366(v19.0.0,简化)
function throwException(
root: FiberRoot,
returnFiber: FiberNode | null, // 抛错 Fiber 的父节点
sourceFiber: FiberNode, // 抛出错误的 Fiber
value: any, // 错误对象(thenable 走 Suspense 分支,此处略)
rootRenderLanes: Lanes,
): boolean {
const errorInfo = createCapturedValueAtFiber(value, sourceFiber);
// 沿着 return 指针向上查找最近的 Error Boundary
let workInProgress = returnFiber;
do {
switch (workInProgress.tag) {
case HostRoot: {
// 没有 Error Boundary 兜底 → 在根上打 ShouldCapture,
// 用 createRootErrorUpdate 把根渲染成 null(卸载整棵树)并触发 onUncaughtError
workInProgress.flags |= ShouldCapture;
const lane = pickArbitraryLane(rootRenderLanes);
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
const update = createRootErrorUpdate(workInProgress.stateNode, errorInfo, lane);
enqueueCapturedUpdate(workInProgress, update);
return false;
}
case ClassComponent: {
const ctor = workInProgress.type;
const instance = workInProgress.stateNode;
if (
(workInProgress.flags & DidCapture) === NoFlags &&
(typeof ctor.getDerivedStateFromError === 'function' ||
(instance !== null && typeof instance.componentDidCatch === 'function'))
) {
// 找到了 Error Boundary!不是另起一次 setState,
// 而是在本次 render 内:打 ShouldCapture,塞入一条"捕获更新",
// unwind 回到这个边界后在同一轮里重新 render 它
workInProgress.flags |= ShouldCapture;
const lane = pickArbitraryLane(rootRenderLanes);
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
const update = createClassErrorUpdate(lane);
initializeClassErrorUpdate(update, root, workInProgress, errorInfo);
enqueueCapturedUpdate(workInProgress, update);
return false;
}
break;
}
}
workInProgress = workInProgress.return;
} while (workInProgress !== null);
return false;
}
为什么 Error Boundary 只能用类组件? 因为错误捕获依赖于 componentDidCatch 和 getDerivedStateFromError 这两个生命周期方法——上面的 throwException 只在 ClassComponent 分支里检查它们,函数组件没有对应的入口。截至 React 19,官方仍未提供函数组件形式的错误边界(useErrorBoundary 之类是社区库 react-error-boundary 的 API)。
3.11 Fiber 的内存管理
Fiber 架构在内存管理上有几个精心设计的策略。
策略一:对象池复用
双缓冲的 alternate 机制本身就是一种对象池——Fiber 节点在两次渲染之间被复用,而不是创建后丢弃。这意味着在稳态(没有新增/删除节点的更新)下,Fiber 树不产生新的 Fiber 节点对象。
策略二:延迟删除
当一个组件被卸载时,React 不会立即清理它的 Fiber 节点:
// 示意,对应 packages/react-reconciler/src/ReactChildFiber.js:381-387——源码先取
// returnFiber.deletions,为 null 时新建数组并打上 ChildDeletion flag,否则 push
// 标记删除,而非立即删除
returnFiber.deletions = returnFiber.deletions || [];
returnFiber.deletions.push(childToDelete);
returnFiber.flags |= ChildDeletion;
// 在 Commit 阶段才真正清理(ReactFiberCommitWork.js,v19.0.0,简化)
function commitDeletionEffects(root, returnFiber, deletedFiber: FiberNode) {
// 递归遍历被删子树:解绑 ref、跑 layout effect 的 cleanup、
// 调 componentWillUnmount、从 DOM 中 removeChild
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
// mutation 阶段只切断 return 指针(child/sibling 还要留给 passive effects 用)
detachFiberMutation(deletedFiber);
}
// passive 阶段结束后再彻底断开引用,帮助 GC
function detachFiberAfterEffects(fiber: FiberNode) {
const alternate = fiber.alternate;
if (alternate !== null) {
fiber.alternate = null;
detachFiberAfterEffects(alternate);
}
fiber.child = null;
fiber.deletions = null;
fiber.sibling = null;
fiber.return = null;
fiber.dependencies = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.stateNode = null;
fiber.updateQueue = null;
}
策略三:subtreeFlags 冒泡优化
// 如果子树没有任何副作用,整个子树可以跳过
if ((fiber.subtreeFlags & MutationMask) === NoFlags) {
// 子树中没有需要 commit 的副作用
// 直接跳过,不遍历子树
return;
}
在一个 1000 个节点的树中,如果只有 3 个节点需要更新,subtreeFlags 让 React 只需沿着通往这 3 个节点的分支下探,跳过其余绝大多数节点的遍历——通过检查每个分支节点的 subtreeFlags,快速定位到有副作用的子树。
3.12 Fiber 架构的设计权衡
Fiber 不是免费的午餐。它的设计中包含了明确的权衡。
权衡一:内存开销
每个 React Element 对应一个 Fiber Node,每个 Fiber Node 有 30+ 个字段,在 V8 里是一个不小的对象;再乘以双缓冲的两棵树,节点数上万时 Fiber 树本身就是一笔可观的内存开销。
这个开销在绝大多数应用中可以忽略。但在极端场景(如大型数据可视化、10 万行表格),它可能成为瓶颈。这也是长列表通常要做虚拟化(Virtualization)的原因之一——不在 DOM 中的元素,也不需要 Fiber Node。
权衡二:复杂性
Fiber 架构极大地增加了 React 内部的复杂性。React 15 的 Stack Reconciler 代码量约 3500 行(v15.6.2 src/renderers/shared/stack/reconciler/,不含测试),React 19 的 Fiber Reconciler 超过 40000 行(v19.0.0 packages/react-reconciler/src/*.js,不含测试与 forks)。这种复杂性不仅增加了维护成本,也提高了社区理解 React 内部原理的门槛。
权衡三:同步 vs 异步的语义变化
可中断渲染引入了一个微妙的语义变化:Render 阶段的代码可能被执行多次。 如果高优先级更新打断了低优先级渲染,被打断的渲染会被丢弃并重新开始。这意味着组件函数、useMemo、useReducer 的回调可能执行多次但只有一次的结果被提交。
function Example() {
// ⚠️ 这个 console.log 可能执行多次,但屏幕只更新一次
console.log('render');
// ⚠️ 不要在渲染过程中执行有副作用的操作
// fetchData(); // 错误!可能被调用多次
return <div>...</div>;
}
这就是 React 严格模式(StrictMode)在开发环境下双重调用组件函数的原因——它在帮你提前发现那些依赖于"Render 只执行一次"假设的代码。
3.13 从源码到实践:理解 Fiber 如何帮助你写更好的代码
理解 Fiber 架构不只是为了满足好奇心——它直接影响你写 React 代码的方式。
实践一:为什么 key 如此重要
在 Fiber 的 Reconciliation 过程中,key 是 React 判断一个节点是否可以复用的关键依据:
// Diff 算法中的 key 匹配
function updateSlot(returnFiber, oldFiber, newChild, lanes) {
const key = oldFiber !== null ? oldFiber.key : null;
if (typeof newChild === 'object' && newChild !== null) {
if (newChild.key === key) {
// key 匹配 → updateElement:type 也相同才复用 Fiber、只更新 props,否则新建
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
// key 不匹配 → 返回 null,跳出按位置比对,转入按 key 建 Map 的查找
return null;
}
}
}
key 不稳定(例如每次渲染都随机生成),React 就无法复用 Fiber 节点——它会销毁旧组件的全部状态(包括 useState 的值、DOM 元素、ref),然后创建新的。这不只是"性能问题",更是"正确性问题"。
实践二:为什么 Hooks 有调用顺序的约束
// Hooks 在 Fiber 上的存储
// memoizedState 是一个链表,每个节点对应一个 Hook 调用
// 链表的顺序就是代码中 Hook 调用的顺序
// 第一次渲染
useState(0) → 链表节点 1: { memoizedState: 0, next: → }
useEffect() → 链表节点 2: { memoizedState: effect, next: → }
useState('') → 链表节点 3: { memoizedState: '', next: null }
// 第二次渲染
// React 按顺序遍历链表,匹配每个 Hook
useState(0) → 读取链表节点 1 → state = 0 ✅
useEffect() → 读取链表节点 2 → effect ✅
useState('') → 读取链表节点 3 → state = '' ✅
// 如果第二次渲染跳过了一个 Hook(条件调用)
useState(0) → 读取链表节点 1 → state = 0 ✅
// useEffect() 被跳过
useState('') → 读取链表节点 2 → 读到了 effect!💥 错误!
实践三:为什么 useEffect 的 cleanup 是异步的
// Fiber 的 Commit 阶段流程
commitBeforeMutationEffects(); // snapshot
commitMutationEffects(); // DOM 操作
root.current = finishedWork; // 切换双缓冲
commitLayoutEffects(); // useLayoutEffect 回调(同步)
// useEffect 的 cleanup 和回调默认在 Commit 之后异步执行
// (含 SyncLane 的更新会在 commit 收尾同步 flush,见第 6 章)
scheduleCallback(NormalPriority, () => {
flushPassiveEffects(); // useEffect
});
useEffect 是异步的,因为它不需要阻塞渲染。useLayoutEffect 是同步的,因为它可能需要在浏览器绘制之前测量 DOM 尺寸。这个区分直接来自 Fiber 的两阶段设计——Commit 阶段内的操作是同步的,Commit 阶段后的操作可以异步调度。
3.14 实测:React 19 Fiber type 字段数 + Lane 常量数
§3.3 介绍 Fiber Node 的"30+ 个字段"——把它在 packages/react-reconciler/src/ReactInternalTypes.js 实测——
type Fiber = { ... } 实测 27 个字段——
// 实测 ReactInternalTypes.js 的 Fiber type 定义(27 个字段)
- 身份:tag, key, elementType, type, stateNode
- 树结构:return, child, sibling, index
- 状态/refs:ref, refCleanup, pendingProps, memoizedProps,
updateQueue, memoizedState, dependencies
- 副作用/优先级:mode, flags, subtreeFlags, deletions, lanes,
childLanes
- 双缓冲:alternate
- Profiler:actualDuration, actualStartTime, selfBaseDuration,
treeBaseDuration
(dev-only 字段还有 _debugInfo / _debugOwner / _debugStack /
_debugTask / _debugIsCurrentlyTiming / _debugNeedsRemount /
_debugHookTypes 共 7 个,不计入主表)
章节 §3.3 "30+ 字段"成立——实测主结构 27 + dev 字段 7 = 34。
packages/react-reconciler/src/ReactFiberLane.js 教学分组约 14 类 Lane(含多位掩码组;不是「导出标识符恰好 14 个」)——
| 常量 / 分组 | 二进制(v19 示意) | 用途 |
|---|---|---|
NoLane |
0b...0000 |
占位 |
SyncHydrationLane |
0b...0001 |
同步 hydration |
SyncLane |
0b...0010 |
同步更新(最高优先级) |
InputContinuousHydrationLane |
0b...0100 |
输入持续 hydration |
InputContinuousLane |
0b...1000 |
拖拽 / 滚动 |
DefaultHydrationLane / DefaultLane |
默认优先级 | |
TransitionHydrationLane / TransitionLanes(多位) |
startTransition | |
RetryLanes(多位) |
Suspense retry | |
SelectiveHydrationLane |
用户交互触发的 hydration | |
IdleHydrationLane / IdleLane |
空闲优先级 | |
OffscreenLane |
0b010…(bit 29) |
Offscreen / 预渲染(REACT_OFFSCREEN_TYPE;公开实验 API 为 unstable_Activity) |
DeferredLane |
0b100…(bit 30) |
useDeferredValue 等延迟渲染(勿与 OffscreenLane 写反) |
TotalLanes |
常量 31 | §3.3 提到的"31 位整数"实测确认 ✓ |
关键 4 文件实测(React 19 源码约数,随 commit 浮动)——
| 文件 | 约行数 |
|---|---|
ReactFiber.js |
约 963(createFiber / createWorkInProgress / allocate) |
ReactInternalTypes.js |
约 465(Fiber type 27 字段定义) |
ReactFiberLane.js |
约 1202(§3.3 Lane 模型 + 位运算 helper) |
ReactFiberWorkLoop.js |
约 4343(§3.5 工作循环主体) |
| 本章核心 4 文件合计 | 约 6970 |
两条值得记住的物理事实——
TotalLanes = 31(不是 32)——ReactFiberLane.js顶部硬编码TotalLanes = 31——JavaScript 的位运算按 32 位有符号整数进行,只用低 31 位可以避开符号位(否则最高位车道的值是负数,比较大小等操作会出问题)——§3.3 的 Lane 模型用位运算实现 O(1) 优先级调度,在源码层就受这个 31 位限制约束——是 JS 位运算语义决定的工程边界ReactFiberLane.js约 1200 行实现教学分组约 14 类 Lane + 30+ 个位运算 helper——配套 mergeLanes / removeLanes / pickArbitraryLane / getNextLanes 等——印证 Lane 不只是一组常量、而是一整套 bitmap 算法库;位图上 Offscreen =0b010…、Deferred =0b100…
串联 ch11 §11.7.4 jsx-runtime、ch14 §14.9 events、ch15 §15.5.4 dom client、ch17 §17.5.5 reconciler 与本节 Fiber 核心 4 文件——本章 Fiber 核心是 reconciler 的物质起点(行数账本以「约」为准,勿抠个位)。
3.15 本章小结
本章深入剖析了 React 最核心的架构——Fiber。总结以下几个关键认知:
第一,Fiber 诞生于 Stack Reconciler 的局限。 JavaScript 调用栈不可中断,导致大型组件树的渲染会阻塞主线程数百毫秒。Fiber 将递归转化为迭代,用链表替代调用栈,实现了可中断的渲染。
第二,Fiber Node 是 React 的"进程控制块"。 它的 30+ 个字段涵盖了身份标识、树结构、状态管理、副作用标记、优先级调度和双缓冲六个维度。每个字段都有明确的设计意图。
第三,双缓冲是 Fiber 的核心策略。 current 树和 workInProgress 树交替使用,在后台完成所有计算工作后一次性提交,避免了用户看到中间状态。Fiber 节点通过 alternate 指针在两棵树之间复用,大幅减少了 GC 压力。
第四,Fiber 的工作循环是"递-归"式的深度优先遍历。 beginWork 向下处理每个节点(递),completeWork 向上创建 DOM(归)。每个节点的处理都是一个独立的"工作单元",并发渲染时可以在任意两个工作单元之间暂停。
第五,Lane 模型用位运算实现了 O(1) 的优先级调度。 31 位整数的每一位代表一个优先级车道,位运算让合并、交集、最高优先级查找都在常数时间内完成。
第六,Fiber 本质上是一个运行在浏览器中的操作系统。 时间切片对应时间片轮转,Lane 对应优先级队列,双缓冲对应进程上下文保存,Render/Commit 对应用户态/内核态。
下一章深入 Scheduler——Fiber 时间切片的具体实现,以及 Lane 与 Scheduler 优先级之间的映射关系。
思考题
-
概念理解:本章将 Fiber 类比为操作系统。请进一步思考:React 的
StrictMode在这个类比中对应什么?提示:考虑操作系统中的安全检查和沙箱机制。 -
实践应用:在你的 React 项目中,使用 React DevTools 的 Profiler 录制一次渲染过程。观察 Fiber 树的遍历顺序——是否与本章描述的"递-归"模式一致?是否有节点因为 bailout 而被跳过?
-
源码探索:阅读
packages/react-reconciler/src/ReactFiberWorkLoop.js中workLoopConcurrent的完整实现。找到shouldYield的判断逻辑——时间片的长度是固定的吗?在什么情况下会动态调整? -
架构对比:Vue 3 没有 Fiber 架构,但它有"异步更新队列"(nextTick)。对比 React 的 Fiber 时间切片和 Vue 3 的异步批处理,它们各自适合什么场景?在什么场景下 React 的方案更优?在什么场景下 Vue 的方案更优?
-
性能分析:Fiber 的双缓冲意味着内存中始终存在两棵树。在一个 10000 节点的应用中,粗略估算双缓冲的内存开销。这个开销在什么类型的设备上可能成为问题?React 提供了哪些机制来缓解这个问题?