Fiber 架构的核心是 Fiber 节点。React 在运行时会将每个组件、每个 HTML 元素都抽象成一个 Fiber 对象,从而形成一个可遍历、可中断的 Fiber 树。理解 Fiber 节点的数据结构,是掌握 React 底层渲染机制的关键一步。
Fiber 节点的“身份证”:一个 JavaScript 对象
Fiber 节点本质上是一个普通的 JavaScript 对象,内部包含了几十个属性。我们可以将这些属性按功能大致分为三类:描述节点信息、构建树形关系、记录工作进度。
下面是 Fiber 节点核心属性的简化定义:
type Fiber = {
// 节点身份标识
tag: WorkTag, // 节点类型(函数组件、类组件、原生标签等)
key: null | string, // 列表中的 key,用于 Diff
elementType: mixed, // 原始类型,如 "div" 或 组件函数
type: mixed, // 经过包装后的类型,通常与 elementType 相同
// 树形关系(重要!)
return: Fiber | null, // 指向父节点
child: Fiber | null, // 指向第一个子节点
sibling: Fiber | null, // 指向下一个兄弟节点
// 工作进度与状态
pendingProps: mixed, // 本轮渲染的新 Props
memoizedProps: mixed, // 上一轮渲染后的 Props
memoizedState: mixed, // 上一次渲染时的状态(类组件为 state,函数组件为 Hooks 链表)
updateQueue: mixed, // 状态更新队列
flags: Flags, // 副作用标记(插入、更新、删除等)
subtreeFlags: Flags, // 子树中存在的副作用标记
// 双缓存
alternate: Fiber | null, // 指向另一个缓冲树中的对应节点
// 调度优先级
lanes: Lanes, // 节点涉及的更新优先级
childLanes: Lanes, // 子树中存在的更新优先级
//... 还有更多内部属性
};
构建 Fiber 树的三个关键属性:return、child、sibling
Fiber 节点通过三个指针编织成一个树状结构,而且支持高效的单向遍历:
return:指向父节点。每当当前节点处理完成后,React 会沿着return回到父节点,继续处理父节点的下一个兄弟。child:指向第一个子节点。当进入一个节点时,React 会先处理它的第一个子节点(深度优先)。sibling:指向右侧的兄弟节点。当所有子节点处理完毕后,React 通过sibling向右移动,处理同级的下一个节点。
这种结构将传统的多叉树转换为类似二叉树的链表结构,遍历时不需要额外的栈或递归,只需按固定的顺序移动指针(先 child,再 sibling,再 return),非常适合可中断的“时间切片”机制。
举个可视化的例子,假设我们有这样一段 JSX:
<div>
<h1>标题</h1>
<p>段落</p>
</div>
对应的 Fiber 树关系如下(箭头方向代表指针指向):
Fiber(div)
│ child
▼
Fiber(h1) ── sibling ──► Fiber(p)
│ │
└── return ──────────────┘
div的child指向h1(第一个子节点)。h1的sibling指向p(下一个兄弟节点)。h1和p的return都指回div(父节点)。- 文本节点“标题”和“段落”也会包装成 Fiber 节点,作为
h1或p的child,这里省略以显清晰。
工作进度记录:memoizedState、memoizedProps 与 updateQueue
Fiber 节点不仅记录结构,还保存渲染过程中的“状态快照”和“待办事项”:
memoizedProps:上一次提交到真实 DOM 时的 Props。在 Render 阶段,新 Props(pendingProps)会与之比较,决定组件是否需要更新。memoizedState:上一次提交时的状态。对于函数组件,这个字段不是简单的值,而是一个 Hooks 链表头。每个 Hook 对应的值(如useState的数据、useReducer的数据)都串联在这个链表上,调用顺序决定了链表节点的位置。updateQueue:存储状态更新任务。例如setState传进来的参数会被加入一个循环链表,然后在 Render 阶段被依次处理。这解释了为什么多次调用setState可以被合并:它们都暂存在同一个updateQueue中。flags(旧称effectTag):标记当前节点需要执行的 DOM 操作,比如插入(Placement)、更新(Update)、删除(Deletion)。Commit 阶段会遍历这些 Flag 并执行对应操作。
双缓存的关键:alternate 属性
React 同时维护两棵 Fiber 树:current 树(当前屏幕上对应的树)和 workInProgress 树(正在内存中构建的新树)。alternate 属性就是这两棵树中对应节点的相互引用。
- 当开始一次新渲染时,React 会为每个 current 节点克隆一个 workInProgress 节点,并将双方通过
alternate互指。 - 在 Render 阶段,所有工作都在 workInProgress 树上进行,current 树保持不变(确保屏幕上的内容不受影响)。
- Commit 阶段完成后,workInProgress 树会变成新的 current 树,旧的 current 树则被标记为“可回收”。
这种双缓冲机制使得并发渲染成为可能:一个高优先级的更新可以打断正在进行的低优先级渲染,因为 current 树负责显示,workInProgress 树可以随时丢弃并从头开始。
节点类型标识:tag 属性
tag 是一个数值,用于区分 Fiber 节点的种类。React 对不同种类的节点处理逻辑不同。常见的 tag 类型包括:
| tag 值 | 含义 |
|--------|------|
| FunctionComponent | 函数组件 |
| ClassComponent | 类组件 |
| HostComponent | 原生 DOM 元素(如 <div>、<span>) |
| HostText | 文本节点 |
| Fragment | <Fragment> 或 <>...</> |
| HostRoot | 容器的根节点(ReactDOM.createRoot 的宿主) |
| ForwardRef | forwardRef 包裹的组件 |
React 在协调(Reconciliation)时会根据 tag 执行不同的处理分支,例如函数组件需要执行 Hook 调用,类组件需要实例化并调用生命周期,原生 DOM 标签则直接生成对应的元素。
为什么这么设计:为“可中断渲染”而生
Fiber 节点的数据结构并非随意设计,而是为了满足两个核心需求:
- 可中断的深度优先遍历:通过
return-child-sibling形成链表,React 可以在任何 Fiber 节点暂停工作,并在浏览器空闲时继续。传统递归无法做到这一点。 - 最小化的状态回溯:每个节点都记录了当前渲染过程中的临时状态(
pendingProps、updateQueue等),即使工作被打断,也能根据这些记录重新开始,无需重头再来。
这种精妙的数据结构是 React 并发能力的基石。理解了 Fiber 节点,你就理解了 React 如何在“快”和“稳”之间取得平衡——既保证了大型应用的流畅性,又不牺牲开发体验。