Appearance
第3章 Fiber 架构:React 的操作系统
本章要点
- Fiber 诞生的历史背景:Stack Reconciler 的致命缺陷与浏览器渲染机制的冲突
- Fiber 数据结构的完整剖析:30+ 字段的设计意图与相互关系
- 双缓冲(Double Buffering)机制:current 树与 workInProgress 树的交替策略
- Fiber 工作循环:从
performUnitOfWork到completeWork的递归拆解- Fiber 与操作系统的类比:进程调度、中断恢复、时间片轮转
- Lane 模型:React 的优先级系统如何用位运算实现 O(1) 调度
2016 年的某一天,React 团队成员 Andrew Clark 在 GitHub 上提交了一份名为 "React Fiber Architecture" 的文档。文档开头只有一句话:
"React Fiber is an ongoing reimplementation of React's core algorithm."
这个看似温和的描述背后,是 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。
typescript
// 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 执行超过了这个时间窗口,浏览器就没有时间执行后续的渲染步骤——用户看到的就是"卡顿"。
图 3-1:Stack Reconciler vs Fiber Reconciler 的时间线对比
让我用一个真实场景说明这个问题有多严重。
大型列表的噩梦
假设你有一个包含 10000 条记录的数据表格。用户在搜索框中输入了一个字符,触发了整个表格的重新过滤和渲染。
tsx
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 团队需要一个根本性的解决方案。
3.2 Fiber 的核心思想:把递归变成迭代
Fiber 的核心思想可以用一句话概括:把不可中断的递归,变成可中断的迭代。
在计算机科学中,任何递归都可以用显式的数据结构(通常是栈或链表)转化为迭代。Fiber 正是这个思想的应用——它用一个链表结构(Fiber Tree)替代了 JavaScript 的调用栈,使得 React 可以在任意时刻"暂停"工作,将控制权交还给浏览器,然后在下一个时间窗口"恢复"工作。
这就像操作系统的上下文切换(Context Switch)——操作系统通过保存和恢复 CPU 寄存器的状态,让多个进程"轮流"使用 CPU,营造出"同时运行"的幻觉。Fiber 通过保存和恢复 Fiber 节点的状态,让 React 的渲染工作和浏览器的渲染工作"轮流"使用主线程。
typescript
// 概念模型:从递归到迭代的转变
// ❌ 递归(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) {
// 还有工作没做完,请求下一个时间片
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 定义
typescript
// packages/react-reconciler/src/ReactInternalTypes.ts(React 19,简化)
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;
// === 状态与更新 ===
pendingProps: any; // 本次更新的新 props
memoizedProps: any; // 上次渲染的 props(已提交)
memoizedState: any; // 上次渲染的 state(Hooks 链表的头节点)
updateQueue: UpdateQueue | null; // 待处理的更新队列
// === 副作用 ===
flags: Flags; // 副作用标记(Placement, Update, Deletion, etc.)
subtreeFlags: Flags; // 子树的副作用标记(冒泡优化)
deletions: FiberNode[] | null; // 需要删除的子 Fiber
// === 调度与优先级 ===
lanes: Lanes; // 本节点上挂起的更新优先级
childLanes: Lanes; // 子树中挂起的更新优先级
// === 双缓冲 ===
alternate: FiberNode | null; // 指向另一棵树中的对应节点
// === 调试(开发模式)===
_debugOwner?: FiberNode | null;
_debugSource?: Source | null;
}这个数据结构可以分成六个维度来理解:
维度一:身份标识
| 字段 | 类型 | 作用 |
|---|---|---|
tag | WorkTag(枚举) | 标识 Fiber 的类型,决定 React 如何处理这个节点 |
key | string | null | Diff 算法中用于识别列表元素的唯一标识 |
elementType | any | React Element 原始的 type 值 |
type | any | 解析后的 type(lazy 组件会被解析为实际组件) |
stateNode | any | 对应的宿主实例(DOM 节点、组件实例) |
tag 字段是 React 的"类型分发器"。React 19 中定义了超过 25 种 WorkTag:
typescript
// packages/react-reconciler/src/ReactWorkTags.ts
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 树不是传统的多叉树,而是通过三个指针实现的链表树:
图 3-2:Fiber 树的链表结构
typescript
// 三指针结构
// App.child → Header
// Header.sibling → Content
// Content.sibling → Footer
// Header.return → App
// Content.return → App
// Footer.return → App为什么不用 children: FiberNode[] 数组?因为链表结构有两个关键优势:
优势一:O(1) 的插入和删除。 数组的插入和删除是 O(n),链表是 O(1)。在大型列表的重排序中,这个差异是实质性的。
优势二:天然支持深度优先遍历。 Fiber 的工作循环需要深度优先遍历整棵树,链表结构让这个遍历可以不使用递归完成——只需要沿着 child 向下、沿着 sibling 向右、沿着 return 向上。
typescript
// 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;
}
}
}维度三:状态与更新
typescript
// 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 的"设计缺陷",而是链表结构的必然约束——在 O(1) 空间复杂度和 O(1) 访问时间的约束下,顺序是唯一的标识方式。
维度四:副作用标记
typescript
// packages/react-reconciler/src/ReactFiberFlags.ts
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) 的查询和修改:
typescript
// 添加标记
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,那么整个子树可以跳过,不需要遍历。 这在大型组件树中是一个重大优化。