React 19 内核探秘

第3章 Fiber 架构:React 的操作系统

作者 杨艺韬 · 10,379 字

第3章 Fiber 架构:React 的操作系统

本章要点

  • Fiber 诞生的历史背景:Stack Reconciler 的致命缺陷与浏览器渲染机制的冲突
  • Fiber 数据结构的完整剖析:30+ 字段的设计意图与相互关系
  • 双缓冲(Double Buffering)机制:current 树与 workInProgress 树的交替策略
  • Fiber 工作循环:从 performUnitOfWorkcompleteWork 的递归拆解
  • 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。

// 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 团队需要一个根本性的解决方案。

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) {
    // 还有工作没做完,请求下一个时间片
    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.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;
}

这个数据结构可以分成六个维度来理解:

维度一:身份标识

字段类型作用
tagWorkTag(枚举)标识 Fiber 的类型,决定 React 如何处理这个节点
keystring | nullDiff 算法中用于识别列表元素的唯一标识
elementTypeanyReact Element 原始的 type
typeany解析后的 type(lazy 组件会被解析为实际组件)
stateNodeany对应的宿主实例(DOM 节点、组件实例)

tag 字段是 React 的”类型分发器”。React 19 中定义了超过 25 种 WorkTag:

// 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 树不是传统的多叉树,而是通过三个指针实现的链表树

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) 的插入和删除。 数组的插入和删除是 O(n),链表是 O(1)。在大型列表的重排序中,这个差异是实质性的。

优势二:天然支持深度优先遍历。 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 的”设计缺陷”,而是链表结构的必然约束——在 O(1) 空间复杂度和 O(1) 访问时间的约束下,顺序是唯一的标识方式。

维度四:副作用标记

// 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) 的查询和修改:

// 添加标记
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.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;
export const TransitionLane1: Lane =           0b0000000000000000000000010000000;
export const TransitionLane2: Lane =           0b0000000000000000000000100000000;
// ... 更多 Transition Lanes
export const IdleLane: Lane =                  0b0010000000000000000000000000000;
export const OffscreenLane: Lane =             0b0100000000000000000000000000000;

Lane 模型是 React 18 引入的优先级系统,替代了 React 16-17 中的 expirationTime 模型。它用 31 位整数的每一位表示一个优先级”车道”(Lane),优先级从低位到高位递减。

Lane优先级触发场景示例
SyncLane最高离散事件onClickonKeyDown
InputContinuousLane连续事件onMouseMoveonScroll
DefaultLane普通更新setTimeoutfetch 回调
TransitionLanesTransition 更新startTransition 包裹的更新
IdleLane最低空闲更新预加载、后台同步

为什么从 expirationTime 迁移到 Lanes?

expirationTime 模型用一个时间戳表示优先级——时间越早到期,优先级越高。这个模型有两个缺陷:

  1. 无法表示一组不连续的优先级。 如果你想处理”优先级 3 和优先级 7,但跳过优先级 5”,expirationTime 做不到。Lanes 可以——lane3 | lane7 表示同时处理两个优先级。

  2. 无法表示一批更新。 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.ts(简化)

// 创建 workInProgress 节点
function createWorkInProgress(current: FiberNode, pendingProps: any): FiberNode {
  let workInProgress = current.alternate;

  if (workInProgress === null) {
    // 首次渲染:创建新的 Fiber 节点
    workInProgress = createFiber(current.tag, pendingProps, current.key);
    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;
  }

  // 复制不变的字段
  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 节点——只更新它的属性。这意味着整个 Fiber 树在首次渲染后就不再产生新的 Fiber Node 对象(除非有新增节点),极大地减少了 GC 压力。

Commit 阶段的切换

当 workInProgress 树构建完成后,React 进入 Commit 阶段,将变更应用到真实 DOM。Commit 完成后,只需要一行代码完成树的切换:

// packages/react-reconciler/src/ReactFiberWorkLoop.ts(简化)
function commitRoot(root: FiberRoot) {
  // ... 应用所有 DOM 变更 ...

  // 🎯 关键一行:切换 current 指针
  root.current = finishedWork;
  // finishedWork 就是 workInProgress 树的根节点
  // 现在它变成了 current 树
  // 而之前的 current 树通过 alternate 指针,成为了下一次更新的 workInProgress 树
}

这行代码完成后:

  • 之前的 workInProgress 树变成了新的 current 树(屏幕上显示的状态)
  • 之前的 current 树通过 alternate 指针,成为了下一次更新时的 workInProgress 树(被复用)

这就是双缓冲的精髓——两棵树在每次更新后交换角色,永远复用,永远不需要重新创建。

💡 最佳实践:理解双缓冲机制解释了 React DevTools 中一个常见的困惑——为什么某些组件的 Fiber 节点在 Profiler 中显示为 “Did not render”,但它们的 alternate 仍然存在。这不是内存泄漏,而是双缓冲的正常行为。React 保留了 alternate 节点以便下次更新时复用。

3.5 Fiber 工作循环:深度优先的可中断遍历

Fiber 的工作循环(Work Loop)是整个架构的核心执行引擎。它将一次完整的树遍历分解为一个个”工作单元”(Unit of Work),每个工作单元处理一个 Fiber 节点。

两阶段:Render 与 Commit

React 的更新过程分为两个阶段:

阶段是否可中断做什么涉及真实 DOM
Render 阶段✅ 可中断构建 workInProgress 树,计算 diff,标记副作用❌ 不涉及
Commit 阶段❌ 不可中断将副作用应用到真实 DOM,执行生命周期✅ 涉及

Render 阶段可中断是 Fiber 的核心价值所在。Commit 阶段不可中断,因为 DOM 操作必须保持原子性——用户不应该看到”更新了一半”的界面。

performUnitOfWork:向下递

// packages/react-reconciler/src/ReactFiberWorkLoop.ts(简化)
function performUnitOfWork(unitOfWork: FiberNode): FiberNode | null {
  const current = unitOfWork.alternate;

  // "递"阶段:处理当前节点,返回子节点
  const next = beginWork(current, unitOfWork, renderLanes);

  // 将 pendingProps 提升为 memoizedProps(本次处理完毕)
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    // 没有子节点了,进入"归"阶段
    completeUnitOfWork(unitOfWork);
  }

  return next; // 返回子节点作为下一个工作单元
}

beginWork:根据类型分发处理

beginWork 是 Render 阶段最重要的函数——它根据 Fiber 的 tag 决定如何处理这个节点:

// packages/react-reconciler/src/ReactFiberBeginWork.ts(简化)
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 && !hasScheduledUpdateOnFiber(current, renderLanes)) {
      // Bailout:跳过这个节点和它的子树
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  }

  // 根据 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。这就是为什么 React.memo 和 React Compiler 的自动记忆化如此重要——它们让 oldProps === newProps 更频繁地成立。

completeUnitOfWork:向上归

beginWork 返回 null(没有子节点了),进入”归”阶段:

// packages/react-reconciler/src/ReactFiberWorkLoop.ts(简化)
function completeUnitOfWork(unitOfWork: FiberNode) {
  let completedWork: FiberNode | null = unitOfWork;

  while (completedWork !== null) {
    const current = completedWork.alternate;

    // "归"阶段的核心:创建或更新 DOM 节点
    completeWork(current, completedWork, renderLanes);

    // 副作用标记冒泡
    const returnFiber = completedWork.return;
    if (returnFiber !== null) {
      returnFiber.subtreeFlags |= completedWork.subtreeFlags | completedWork.flags;
    }

    // 有兄弟节点?返回兄弟,继续"递"
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return; // 退出 complete 循环,回到 performUnitOfWork
    }

    // 没有兄弟,继续向上"归"
    completedWork = returnFiber;
    workInProgress = completedWork;
  }
}

completeWork:创建真实 DOM

completeWork 在”归”阶段为 HostComponent(原生 DOM 元素)创建真实 DOM 节点:

// packages/react-reconciler/src/ReactFiberCompleteWork.ts(简化)
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) {
        // 更新:对比新旧 props,生成 updatePayload
        const updatePayload = prepareUpdate(
          workInProgress.stateNode,
          type,
          current.memoizedProps,
          workInProgress.pendingProps
        );
        if (updatePayload !== null) {
          workInProgress.flags |= Update;
        }
      } else {
        // 首次渲染:创建 DOM 节点
        const instance = createInstance(type, workInProgress.pendingProps);
        // 将所有子 DOM 节点挂载到这个节点下
        appendAllChildren(instance, workInProgress);
        workInProgress.stateNode = instance;
      }
      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);
      }
      return null;
    }
    // ... 其他类型
  }
}

完整的遍历流程

让我用一个具体的组件树来展示完整的”递-归”遍历流程:

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.ts(简化)

// 同步模式:不可中断
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// 并发模式:可中断
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

唯一的区别就是 shouldYield() 这个检查。shouldYield 来自 React 的调度器(Scheduler),它检查当前时间片是否已用完:

// packages/scheduler/src/forks/Scheduler.ts(简化)
function shouldYieldToHost(): boolean {
  const currentTime = getCurrentTime();
  return currentTime >= deadline;
  // deadline = startTime + yieldInterval
  // yieldInterval 默认为 5ms
}

5 毫秒的时间片。 React 的默认时间片是 5ms——这意味着每处理一个 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 模型高优先级更新(用户输入)可以打断低优先级更新(数据加载)
抢占式调度高优先级 Lane 打断低优先级渲染类似于 Unix 的信号中断
双缓冲帧current 树 / workInProgress 树后台计算,一次性切换到前台
虚拟内存/页表Fiber 树的链表结构通过指针间接访问,支持动态调整
内核态/用户态Render 阶段 / Commit 阶段Render 可中断(用户态),Commit 不可中断(内核态)

🔥 深度洞察:React 是一个运行在浏览器中的操作系统

这不是一个隐喻——React 团队在设计 Fiber 时,明确参考了操作系统的调度理论。Sebastian Markbåge 曾在一次技术分享中说:“We’re essentially building a tiny operating system that runs inside the browser.” 理解这一点,你就能理解 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: uninitializedFiber,     // 指向 HostRootFiber
    finishedWork: null,              // commit 阶段的入口
    pendingLanes: NoLanes,
    // ...
  };

  // HostRootFiber:Fiber 树的根节点
  const hostRootFiber: FiberNode = {
    tag: HostRoot,
    stateNode: fiberRoot,           // 反向指向 FiberRoot
    // ...
  };

  fiberRoot.current = hostRootFiber;
  return fiberRoot;
}

FiberRootHostRootFiber 是两个不同的东西:FiberRoot 是容器,HostRootFiber 是 Fiber 树的根。它们通过 currentstateNode 互相指向。

步骤二:调度更新

// root.render(<App />) 内部
function updateContainer(element: ReactElement, root: FiberRoot) {
  const hostRootFiber = root.current;

  // 创建一个更新对象
  const update: Update = {
    payload: { element }, // element 就是 <App />
    lane: SyncLane,       // 首次渲染使用同步优先级
  };

  // 将更新放入 HostRootFiber 的更新队列
  enqueueUpdate(hostRootFiber, update);

  // 调度渲染
  scheduleUpdateOnFiber(hostRootFiber, SyncLane);
}

步骤三:Render 阶段

1. beginWork(HostRootFiber)
   - 从 updateQueue 中取出 <App /> element
   - 为 App 创建 FiberNode(tag: FunctionComponent)
   - 返回 App Fiber 作为 child

2. beginWork(App Fiber)
   - 调用 App 函数组件
   - useState(0) 初始化 Hook 链表,memoizedState = 0
   - 组件返回 JSX → React Element 树
   - 为 <div> 创建 FiberNode(tag: HostComponent)
   - 返回 div Fiber 作为 child

3. beginWork(div Fiber)
   - 处理 className="app" 等 props
   - 为 <h1> 创建 FiberNode
   - 返回 h1 Fiber 作为 child

4. beginWork(h1 Fiber) → beginWork("Count: 0" Text)
   - 叶子节点,返回 null

5. completeWork("Count: 0" Text) → 创建文本节点
6. completeWork(h1 Fiber) → 创建 <h1> DOM,挂载文本
7. beginWork(button Fiber) → beginWork("+1" Text)
8. completeWork("+1" Text) → 创建文本节点
9. completeWork(button Fiber) → 创建 <button> DOM,挂载文本,绑定 onClick
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 的回调(异步,不阻塞渲染)
}

3.9 更新(Update)的完整流程

当用户点击按钮触发 setCount(c => c + 1) 时,更新流程与首次渲染有几个关键差异:

差异一:复用而非创建

// 创建 workInProgress 时复用 alternate
const workInProgress = createWorkInProgress(current, pendingProps);
// 如果 current.alternate 存在,直接复用
// 不需要创建新的 Fiber 对象

差异二:Bailout 优化

// beginWork 中的 bailout 检查
if (oldProps === newProps && !hasScheduledUpdate) {
  // 这个节点和它的子树都不需要更新
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

在上面的例子中,<button>onClick 函数如果在每次渲染时都创建新的引用,button Fiber 就不会命中 bailout。但如果使用 useCallback 缓存(或 React Compiler 自动缓存),button 及其子树可以被跳过。

差异三:DOM 更新而非创建

// completeWork 中的更新路径
if (current !== null && workInProgress.stateNode !== null) {
  // 已有 DOM 节点,只需要对比属性变更
  const updatePayload = prepareUpdate(instance, type, oldProps, newProps);
  // updatePayload = ['children', 'Count: 1']
  // 只记录变化的属性,不重新创建 DOM
}

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.ts(简化)
function throwException(
  root: FiberRoot,
  sourceFiber: FiberNode,  // 抛出错误的 Fiber
  value: any               // 错误对象
) {
  // 沿着 return 指针向上查找最近的 Error Boundary
  let workInProgress = sourceFiber.return;

  while (workInProgress !== null) {
    if (workInProgress.tag === ClassComponent) {
      const instance = workInProgress.stateNode;
      if (typeof instance.componentDidCatch === 'function' ||
          typeof workInProgress.type.getDerivedStateFromError === 'function') {
        // 找到了 Error Boundary!
        // 在这个节点上调度一个错误更新
        const update = createClassErrorUpdate(workInProgress, value);
        enqueueUpdate(workInProgress, update);
        scheduleUpdateOnFiber(workInProgress, SyncLane);
        return;
      }
    }
    workInProgress = workInProgress.return;
  }

  // 没有找到 Error Boundary → 整个应用崩溃
  // React 会卸载整个组件树
}

为什么 Error Boundary 只能用类组件? 因为错误捕获依赖于 componentDidCatchgetDerivedStateFromError 这两个生命周期方法。React 团队曾考虑为函数组件添加类似的 Hook(如 useErrorBoundary),但截至 React 19 仍未实现。原因之一是函数组件的执行是 Render 阶段的一部分,而错误恢复需要在当前渲染之外调度新的更新——这在类组件的实例方法模型中更自然。

3.11 Fiber 的内存管理

Fiber 架构在内存管理上有几个精心设计的策略。

策略一:对象池复用

双缓冲的 alternate 机制本身就是一种对象池——Fiber 节点在两次渲染之间被复用,而不是创建后丢弃。这意味着在稳态(没有新增/删除节点的更新)下,Fiber 树不产生任何新的对象。

策略二:延迟删除

当一个组件被卸载时,React 不会立即清理它的 Fiber 节点:

// 标记删除,而非立即删除
returnFiber.deletions = returnFiber.deletions || [];
returnFiber.deletions.push(childToDelete);
returnFiber.flags |= ChildDeletion;

// 在 Commit 阶段才真正清理
function commitDeletion(fiber: FiberNode) {
  // 递归清理子树的 ref、effect、stateNode
  commitNestedUnmounts(fiber);
  // 从 DOM 中移除
  removeChild(parentDom, fiber.stateNode);
  // 断开 Fiber 节点的引用,帮助 GC
  fiber.return = null;
  fiber.child = null;
  fiber.stateNode = null;
  fiber.alternate = null;
}

策略三:subtreeFlags 冒泡优化

// 如果子树没有任何副作用,整个子树可以跳过
if ((fiber.subtreeFlags & MutationMask) === NoFlags) {
  // 子树中没有需要 commit 的副作用
  // 直接跳过,不遍历子树
  return;
}

在一个 1000 个节点的树中,如果只有 3 个节点需要更新,subtreeFlags 让 React 可以跳过 997 个节点的遍历——通过检查每个分支节点的 subtreeFlags,快速定位到有副作用的子树。

3.12 Fiber 架构的设计权衡

Fiber 不是免费的午餐。它的设计中包含了明确的权衡。

权衡一:内存开销

每个 React Element 对应一个 Fiber Node,每个 Fiber Node 有 30+ 个字段。在 V8 引擎中,一个 Fiber Node 大约占用 300-500 字节。对于一个 10000 节点的应用,Fiber 树占用约 3-5MB(乘以 2 因为双缓冲)。

这个开销在绝大多数应用中可以忽略。但在极端场景(如大型数据可视化、10 万行表格),它可能成为瓶颈。这也是 React 团队推动虚拟化(Virtualization)的原因之一——不在 DOM 中的元素,也不需要 Fiber Node。

权衡二:复杂性

Fiber 架构极大地增加了 React 内部的复杂性。React 15 的 Stack Reconciler 代码量约 3000 行,React 19 的 Fiber Reconciler 超过 30000 行。这种复杂性不仅增加了维护成本,也提高了社区理解 React 内部原理的门槛。

权衡三:同步 vs 异步的语义变化

可中断渲染引入了一个微妙的语义变化:Render 阶段的代码可能被执行多次。 如果高优先级更新打断了低优先级渲染,被打断的渲染会被丢弃并重新开始。这意味着组件函数、useMemouseReducer 的回调可能执行多次但只有一次的结果被提交。

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 匹配 → 复用 Fiber,只更新 props
      return updateElement(returnFiber, oldFiber, newChild, lanes);
    } else {
      // key 不匹配 → 不能复用,标记为新增
      return null;
    }
  }
}

没有 key 或 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)  → 读取链表节点 1state = 0
useEffect()  → 读取链表节点 2effect
useState('')  → 读取链表节点 3state = ''

// 如果第二次渲染跳过了一个 Hook(条件调用)
useState(0)  → 读取链表节点 1state = 0
// useEffect() 被跳过
useState('')  → 读取链表节点 2读到了 effect!💥 错误

实践三:为什么 useEffect 的 cleanup 是异步的

// Fiber 的 Commit 阶段流程
commitBeforeMutationEffects(); // snapshot
commitMutationEffects();        // DOM 操作
root.current = finishedWork;    // 切换双缓冲

commitLayoutEffects();          // useLayoutEffect 回调(同步)

// useEffect 的 cleanup 和回调在 Commit 之后异步执行
scheduleCallback(NormalPriority, () => {
  flushPassiveEffects(); // useEffect
});

useEffect 是异步的,因为它不需要阻塞渲染。useLayoutEffect 是同步的,因为它可能需要在浏览器绘制之前测量 DOM 尺寸。这个区分直接来自 Fiber 的两阶段设计——Commit 阶段内的操作是同步的,Commit 阶段后的操作可以异步调度。

3.13.1 实测:React 19 Fiber type 字段数 + Lane 常量数

§3.3 介绍 Fiber Node 的”30+ 个字段”——把它在 packages/react-reconciler/src/ReactInternalTypes.js 实测——

type Fiber = { ... } 实测 25 个字段(不是 30+)——

// 实测 ReactInternalTypes.js 的 Fiber type 定义(25 个字段)
- 身份tag, key, elementType, type, stateNode
- 树结构return, child, sibling, index
- 状态/refsref, refCleanup, pendingProps, memoizedProps,
  updateQueue, memoizedState, dependencies
- 副作用/优先级mode, flags, subtreeFlags, deletions, lanes,
  childLanes
- 双缓冲alternate
- ProfileractualDuration, actualStartTime, treeBaseDuration
dev-only 字段还有 _debugInfo / _debugOwner / _debugStack 等不计入主表

章节 §3.3 “30+ 字段”是粗略说法——实测主结构 25 + dev 字段 ~5 = 接近 30。

packages/react-reconciler/src/ReactFiberLane.js 实测 14 个 Lane 常量——

常量二进制用途
NoLane0b...0000占位
SyncHydrationLane0b...0001同步 hydration
SyncLane0b...0010同步更新(最高优先级)
InputContinuousHydrationLane0b...0100输入持续 hydration
InputContinuousLane0b...1000拖拽 / 滚动
DefaultHydrationLane / DefaultLane默认优先级
TransitionHydrationLane / TransitionLanes(多位)startTransition
RetryLanes(多位)Suspense retry
SelectiveHydrationLane0b001000...用户交互触发的 hydration
IdleHydrationLane / IdleLane最低优先级
OffscreenLane0b1000000...Offscreen 组件(与 ch02 §2.8.1 测得的 REACT_OFFSCREEN_TYPE 对应)
TotalLanes常量 31§3.5 提到的”31 位整数”实测确认 ✓

关键 4 文件实测——

文件
ReactFiber.js929(createFiber / createWorkInProgress / allocate)
ReactInternalTypes.js421(Fiber type 25 字段定义
ReactFiberLane.js898(§3.5 Lane 模型 14 个常量 + 位运算 helper
ReactFiberWorkLoop.js3985(§3.5 工作循环主体)
本章核心 4 文件合计6233

两条值得记住的物理事实——

  1. TotalLanes = 31(不是 32)——ReactFiberLane.js 顶部硬编码 TotalLanes = 31——是因为 JavaScript 的位运算在 31 位之内才能保持 SMI(Small Integer)优化、超过 31 位会退化为 double——§3.5 标题”Lane 模型用位运算实现 O(1) 优先级调度”在源码层就是这个 31 限制——是 V8/SpiderMonkey 引擎实现细节决定的工程边界
  2. ReactFiberLane.js 898 行实现 14 个 Lane 常量 + 30+ 个位运算 helper——平均每个 Lane 常量配套 ~60 行 helper(mergeLanes / removeLanes / pickArbitraryLane / getNextLanes / etc)——印证 §3.5 “Lane 不只是 14 个常量、是一整套 bitmap 算法库

串联 ch11 §11.7.4 jsx-runtime 829 + ch14 §14.8.1 events 3784 + ch15 §15.5.4 dom client 10452 + ch17 §17.5.5 reconciler 36514 + 本节 Fiber 核心 4 文件 6233(包含在 reconciler 36514 里)——本章 Fiber 核心是 reconciler 36514 的 17%、是 React 一切渲染策略的物质起点。

3.14 本章小结

本章深入剖析了 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 优先级之间的映射关系。


思考题

  1. 概念理解:本章将 Fiber 类比为操作系统。请进一步思考:React 的 StrictMode 在这个类比中对应什么?提示:考虑操作系统中的安全检查和沙箱机制。

  2. 实践应用:在你的 React 项目中,使用 React DevTools 的 Profiler 录制一次渲染过程。观察 Fiber 树的遍历顺序——是否与本章描述的”递-归”模式一致?是否有节点因为 bailout 而被跳过?

  3. 源码探索:阅读 packages/react-reconciler/src/ReactFiberWorkLoop.tsworkLoopConcurrent 的完整实现。找到 shouldYield 的判断逻辑——时间片的长度是固定的吗?在什么情况下会动态调整?

  4. 架构对比:Vue 3 没有 Fiber 架构,但它有”异步更新队列”(nextTick)。对比 React 的 Fiber 时间切片和 Vue 3 的异步批处理,它们各自适合什么场景?在什么场景下 React 的方案更优?在什么场景下 Vue 的方案更优?

  5. 性能分析:Fiber 的双缓冲意味着内存中始终存在两棵树。在一个 10000 节点的应用中,粗略估算双缓冲的内存开销。这个开销在什么类型的设备上可能成为问题?React 提供了哪些机制来缓解这个问题?