Fiber 数据结构 — React 的内部表示
前置知识
- ›第 1 篇:架构概览(了解各包的位置以及 fork 系统的工作原理)
- ›JavaScript 位运算(&、|、~、>>>、clz32)
- ›树形数据结构与链表遍历
Fiber 数据结构 — React 的内部表示
你写的每一个 React 组件、渲染的每一个 <div>、包裹的每一个 Suspense 边界,在运行时都会变成一个 Fiber 节点。Fiber 本质上是一个普通的 JavaScript 对象(出于 V8 形状优化的考虑,实际上通过构造函数创建),它承载了 React 处理一个工作单元所需的一切信息:类型、props、state、在树中的位置,以及需要提交到宿主环境的副作用。
理解 Fiber 类型是本系列后续内容的前置知识。工作循环遍历 fiber,Hooks 将状态存储在 fiber 上,提交阶段从 fiber 读取 flags。如果说 fiber 是 React 运行时宇宙的原子,那本篇就是你的元素周期表。
Fiber 类型定义
完整的 Fiber 类型定义在 ReactInternalTypes.js 中。下面按逻辑分组逐一介绍各个字段。
标识字段 — 这个 fiber 是什么?
| 字段 | 类型 | 用途 |
|---|---|---|
tag |
WorkTag |
数字类型标识符(FunctionComponent、HostComponent 等) |
key |
ReactKey |
用户提供的、用于协调的 key |
elementType |
any |
React 元素中原始的 type(协调时用于保持引用稳定) |
type |
any |
已解析的函数/类/字符串——热更新时可能与 elementType 不同 |
stateNode |
any |
HostComponent 对应 DOM 节点;ClassComponent 对应类实例;HostRoot 对应 FiberRoot |
树链接 — 这个 fiber 在树中的位置?
| 字段 | 类型 | 用途 |
|---|---|---|
return |
Fiber | null |
父 fiber(命名为"return",类比调用栈的返回地址) |
child |
Fiber | null |
第一个子 fiber |
sibling |
Fiber | null |
下一个兄弟 fiber |
index |
number |
在兄弟节点中的位置 |
输入/输出 — 流经这个 fiber 的数据是什么?
| 字段 | 类型 | 用途 |
|---|---|---|
pendingProps |
any |
当前渲染的 props |
memoizedProps |
any |
上次完成渲染时的 props |
memoizedState |
any |
上次完成渲染时的 state(对于 hooks:hook 链表的头节点) |
updateQueue |
mixed |
待处理的 state 更新队列 |
调度与副作用 — React 应该何时、如何处理这个 fiber?
| 字段 | 类型 | 用途 |
|---|---|---|
lanes |
Lanes |
待处理工作的优先级位掩码 |
childLanes |
Lanes |
子树中待处理的工作 |
flags |
Flags |
当前 fiber 上的副作用标志 |
subtreeFlags |
Flags |
子树中副作用标志的聚合 |
deletions |
Array<Fiber> | null |
待删除的子节点 |
alternate |
Fiber | null |
该 fiber 在另一棵树中的对应版本(双缓冲) |
树结构 — Child、Sibling、Return
React 不用数组存储子节点,而是使用单向链表:fiber 的 child 指向它的第一个子节点,每个子节点的 sibling 指向下一个兄弟节点,每个 fiber 的 return 则指回父节点。
以这段 JSX 为例:
<App>
<Header />
<Main />
<Footer />
</App>
对应的 fiber 树结构如下:
graph TD
App["App<br/>tag: FunctionComponent"]
Header["Header<br/>tag: FunctionComponent"]
Main["Main<br/>tag: FunctionComponent"]
Footer["Footer<br/>tag: FunctionComponent"]
App -->|child| Header
Header -->|sibling| Main
Main -->|sibling| Footer
Header -->|return| App
Main -->|return| App
Footer -->|return| App
这种链表设计让工作循环(第 3 篇详述)可以在不递归、不分配数组的情况下遍历整棵树。遍历算法是:先走到 child,处理它,然后沿 sibling 链依次处理,直到没有更多兄弟节点,再通过 return 回到父节点,继续处理父节点的 sibling。
FiberNode 构造函数会将这些链接初始化为 null:
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
提示: 该字段叫
return而非parent,是因为它对应调用栈的隐喻。处理一个 fiber 就像调用一个函数,完成后你会"返回"给调用者。一旦内化这个比喻,工作循环的行为就会变得直观易懂。
双缓冲 — Current 与 WorkInProgress
React 同时维护树的两个版本。current 树是当前已提交到屏幕的版本,workInProgress 树是渲染过程中正在构建的版本。
每个 fiber 都有一个 alternate 指针,指向另一棵树中的对应节点。createWorkInProgress 函数会复用已有的 alternate,或者创建一个新的 fiber:
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
// ... set up alternate links
} else {
// Reuse the existing alternate, reset its fields
workInProgress.pendingProps = pendingProps;
// ...
}
}
sequenceDiagram
participant C as Current Tree
participant W as WorkInProgress Tree
participant S as Screen
Note over C,S: Initial state: current is committed
C->>W: createWorkInProgress (clone fibers)
Note over W: Render phase: modify WIP tree
W->>C: commitRoot: swap current pointer
Note over C,S: WIP becomes the new current
C->>S: DOM mutations applied
双缓冲技术正是并发渲染得以实现的基础。current 树保持稳定(可被事件系统、DevTools 等读取),而 workInProgress 树在同步被修改。提交时,React 只需更新 root.current,将指针切换到新树即可。
WorkTag — 30 种 Fiber 类型
每个 fiber 的 tag 字段是一个数字常量,定义在 ReactWorkTags.js 中,用于标识该 fiber 代表哪种组件。beginWork 函数(第 3 篇详述)会通过一个大型 switch 语句按 tag 分发到对应的处理逻辑。
以下是按类别分组的 tag 列表:
| Tag | 值 | 类别 |
|---|---|---|
FunctionComponent |
0 | 基础组件 |
ClassComponent |
1 | 基础组件 |
HostRoot |
3 | 宿主元素 |
HostPortal |
4 | 宿主元素 |
HostComponent |
5 | 宿主元素(如 <div>) |
HostText |
6 | 宿主元素 |
HostHoistable |
26 | 宿主元素(如 <link>、<script>) |
HostSingleton |
27 | 宿主元素(如 <html>、<head>、<body>) |
Fragment |
7 | 结构类 |
Mode |
8 | 结构类 |
ContextProvider |
10 | Context |
ContextConsumer |
9 | Context |
SuspenseComponent |
13 | 边界 |
SuspenseListComponent |
19 | 边界 |
ActivityComponent |
31 | 边界(前身为 Offscreen) |
MemoComponent |
14 | 优化 |
SimpleMemoComponent |
15 | 优化(使用默认比较的函数组件) |
LazyComponent |
16 | 优化 |
ForwardRef |
11 | 包装器 |
Profiler |
12 | 开发工具 |
ViewTransitionComponent |
30 | 过渡动画 |
Throw |
29 | 错误处理 |
注意值 2 被跳过(原 IndeterminateComponent,已在 React 19 中移除),值 20 同样缺失。
副作用 Flags 与阶段掩码
每个 fiber 的 flags 字段是一个 31 位的位掩码,定义在 ReactFiberFlags.js 中,用于告知提交阶段需要执行哪些副作用。
flowchart TD
subgraph DF["Dynamic Flags"]
P["Placement (0b10)<br/>Insert into DOM"]
U["Update (0b100)<br/>Apply prop changes"]
CD["ChildDeletion (0b10000)<br/>Remove children"]
R["Ref (0b1000000000)<br/>Attach/detach refs"]
PA["Passive (0b100000000000)<br/>useEffect"]
V["Visibility (0b10000000000000)<br/>Show/hide"]
end
subgraph PhasM["Phase Masks"]
BM["BeforeMutationMask<br/>Snapshot"]
MM["MutationMask<br/>Placement | Update | ChildDeletion<br/>| Ref | Hydrating | Visibility"]
LM["LayoutMask<br/>Update | Callback | Ref | Visibility"]
PM["PassiveMask<br/>Passive | Visibility | ChildDeletion"]
end
DF --> PhasM
阶段掩码是关键的优化手段。提交阶段,React 会对树进行多次遍历(before-mutation、mutation、layout、passive)。每个子阶段都有一个掩码,定义了它关心哪些 flags。subtreeFlags 字段聚合了整个子树的 flags,这样 React 就能跳过那些没有相关副作用的子树。
例如,MutationMask 定义如下:
export const MutationMask =
Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility | FormReset;
如果某个 fiber 的 subtreeFlags & MutationMask 为零,mutation 子阶段会直接跳过整个子树。
此外还有静态 flags(第 67-88 行),如 LayoutStatic、PassiveStatic 和 ViewTransitionStatic。与每次渲染后都会重置的动态 flags 不同,静态 flags 在 fiber 的整个生命周期内持久存在。它们表示某个 fiber 曾经使用过某类副作用,从而在卸载时实现优化——如果 PassiveStatic 从未被设置,React 就不必遍历整个子树去查找 passive effect 的清理函数。
FiberRoot 与根节点创建
Fiber 节点代表树中的各个组件,而 FiberRoot 则是容器层级的数据结构,持有整棵树的调度与生命周期状态。
FiberRoot 由 FiberRootNode 创建,包含以下字段:
current:已提交的根 fiber(一个 HostRoot fiber)containerInfo:DOM 容器元素pendingLanes、suspendedLanes、pingedLanes、expiredLanes:用于调度的 lane 追踪callbackNode、callbackPriority:与调度器的集成next:链接指针,将有待处理工作的根节点串成链表finishedWork:已完成、等待提交的 workInProgress 树
FiberRoot 与 HostRoot fiber 之间是双向引用关系:
graph LR
FR["FiberRoot<br/>(container-level state)"]
HR["HostRoot Fiber<br/>(tag: 3)"]
FR -->|"current"| HR
HR -->|"stateNode"| FR
HR -->|"child"| App["App Fiber"]
当你调用 createRoot(container) 时,DOM renderer 会通过 createFiberRoot 创建 FiberRoot 及其初始 HostRoot fiber。HostRoot fiber 比较特殊——它始终是树中最顶层的 fiber,是渲染的"入口点"fiber。
Mode 位
每个 fiber 还携带一个 mode 字段,这是一个小型位掩码,定义在 ReactTypeOfMode.js 中:
export const NoMode = 0b0000000;
export const ConcurrentMode = 0b0000001;
export const ProfileMode = 0b0000010;
export const StrictLegacyMode = 0b0001000;
export const StrictEffectsMode = 0b0010000;
Mode 位从父节点继承到子节点,创建后不再改变。ConcurrentMode 开启时间切片;ProfileMode 开启 Profiler 的计时功能;StrictEffectsMode 在开发环境下会双重调用副作用。
提示: 调试 React 内部时,fiber 上最有价值的字段是:
tag(节点类型)、memoizedState(当前 state 或 hook 链表)、flags(待处理的副作用)以及lanes(已调度工作的优先级)。
下一篇
现在你已经理解了 Fiber 数据结构——它的树形结构、双缓冲生命周期、类型 tag 以及 flag 系统。接下来,让我们看它如何运转起来。下一篇文章将追踪工作循环:React 的核心引擎,沿树向下调用 beginWork,向上调用 completeWork,最终将结果提交到屏幕。