~/ ?.log $
返回文章列表
15 min read
更新于 2026年3月6日

我不知道的 React(02)— Fiber 架构

很多人以为 Fiber 只是 React 16 引入的一种新数据结构,但实际上,它是一整套调度系统——包含数据结构、遍历算法、时间分片机制和优先级模型。React 团队花了两年重写核心引擎,不是为了换个数据结构,而是为了让渲染这件事变得"可商量"。

很多人以为 Fiber 只是 React 16 引入的一种新数据结构,但实际上,它是一整套调度系统——包含数据结构、遍历算法、时间分片机制和优先级模型。React 团队花了两年重写核心引擎,不是为了换个数据结构,而是为了让渲染这件事变得”可商量”。

理解 Fiber,就是理解现代 React 一切并发特性的地基。

一、Fiber 之前的世界

React 16 之前的调和算法叫 Stack Reconciliation(栈调和)。名字里的”栈”不是比喻,它真的用 JavaScript 的函数调用栈来递归遍历整棵组件树。

问题出在哪?递归一旦启动,就无法中途停下来。 React 从根节点一路递归到叶子节点,计算完所有 Diff 再统一更新 DOM。如果组件树很深、节点很多,这个过程可能耗时几十甚至上百毫秒。

浏览器的一帧只有约 16ms(60fps)。一次调和占了 50ms,意味着这 50ms 里浏览器无法响应用户输入、无法执行动画回调、无法做任何事。用户看到的就是”卡了”。

这里有一个很多人会忽略的细节——问题不在于 Diff 算法本身慢,而在于整个过程是同步的、不可中断的。哪怕 Diff 算法已经足够高效,只要组件树规模够大,单次执行时间就会超过一帧的预算。更糟糕的是,所有更新一视同仁:用户正在打字触发的更新,和一个后台数据刷新触发的更新,享有同样的执行权——谁先来谁先占住主线程,没有任何优先级可言。

React 团队需要的不是一个更快的 Diff,而是一种能暂停、能恢复、能插队的渲染机制

二、Fiber 到底是什么

Fiber 这个词在 React 语境里同时指三样东西:一种数据结构、一个工作单元、一套调度系统。分开理解比混在一起更清晰。

数据结构:链表替代递归

说白了,Fiber 节点就是一个普通的 JavaScript 对象,用三根指针把整棵组件树串成了链表。

const fiberNode = {
  tag: 'FunctionComponent',
  type: App,
  stateNode: null,

  child: childFiber, // 第一个子节点
  sibling: siblingFiber, // 下一个兄弟节点
  return: parentFiber, // 父节点

  pendingProps: {
    /* 新的 props */
  },
  memoizedState: null,
  flags: 0, // 副作用标记(插入、更新、删除)
  lanes: 0, // 优先级
};

这段结构的关键在于 childsiblingreturn 三个指针。每个 Fiber 节点只记录自己的第一个子节点、下一个兄弟、以及父节点。通过这三根指针,整棵树变成了一条可以用 while 循环遍历的链表。

为什么选链表而不是继续用递归? 因为递归依赖函数调用栈,调用栈由 JavaScript 引擎管理,代码没有能力在递归到一半时”存档”并退出,下次再”读档”继续。而链表遍历的状态全部保存在 Fiber 节点自身——当前处理到哪个节点、父节点是谁、还有哪些兄弟没处理——随时可以停下来,随时可以从上次停下的地方继续。

换句话说,递归和链表遍历的根本区别在于:递归的进度存在调用栈里(引擎控制),链表的进度存在堆内存里(应用控制)

工作单元:每个节点就是一个任务

每个 Fiber 节点同时也是一个工作单元(Unit of Work)。React 处理一个 Fiber 节点时,会执行该组件的渲染逻辑(调用函数组件或 class 的 render 方法),对比新旧 props 和 state,标记需要的 DOM 操作。处理完一个节点,就算完成了一个工作单元。

这套遍历逻辑的简化版如下:

function workLoop(deadline) {
  while (nextUnitOfWork && deadline.timeRemaining() > 1) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
  if (nextUnitOfWork) {
    // 时间用完了,把剩余工作交给下一帧
    scheduleCallback(workLoop);
  } else {
    // 所有工作单元处理完毕,进入 Commit 阶段
    commitRoot();
  }
}

这段伪代码揭示了 Fiber 最核心的能力:while 循环里每处理完一个工作单元,都会检查 deadline.timeRemaining()时间够就继续,时间不够就停下来,把控制权还给浏览器。 这就是”可中断渲染”的全部秘密——不是什么黑魔法,就是把递归拆成了循环,每轮循环都有退出的机会。

调度系统:决定什么时候做、先做什么

光能暂停还不够。暂停之后何时恢复?多个更新同时存在时先处理哪个?这是 Scheduler(调度器)要解决的问题,下面的”时间分片”一节会展开。

三、两阶段革命

Fiber 把原来一步到位的更新过程拆成了两个阶段,这个拆分是一切可中断能力的前提。

Render Phase(渲染阶段) 在内存中遍历 Fiber 树,执行组件渲染、对比新旧状态、标记需要变更的节点。这个阶段不触碰真实 DOM,纯粹是内存里的计算。正因为没有副作用,所以可以随时暂停、随时丢弃重来。

Commit Phase(提交阶段) 拿到 Render 阶段算好的变更列表,一次性把所有 DOM 操作执行完。这个阶段同步且不可中断

如果你只记住一句话,记住这个:Render 阶段可以被打断一百次,但 Commit 阶段必须一气呵成。 这保证了用户永远不会看到”更新了一半”的 UI。

问题的关键在于——为什么 Commit 必须同步?想象一下,如果 DOM 更新执行到一半被中断:列表里的第 3 项已经删了,但第 5 项还没插入。用户看到的是一个残缺的界面。所以 Render 阶段负责计算(可以慢慢来),Commit 阶段负责执行(必须快准狠)。

四、时间分片:如何把大任务切碎

Render 阶段的”可中断”不是自动发生的,背后是 Scheduler 在管理每一帧的时间预算。

Scheduler 的核心逻辑可以用一句话概括:在浏览器每帧的空闲时间里尽量多做一些 Fiber 工作单元,一旦超时就主动让出主线程。 早期的实现尝试过 requestIdleCallback,但它的调用频率太低且不稳定。React 最终自己实现了一套基于 MessageChannel 的调度机制,默认时间片大约 5ms。

const channel = new MessageChannel();
const port = channel.port2;

channel.port1.onmessage = () => {
  const currentTime = performance.now();
  // deadline 约 5ms
  const deadline = currentTime + 5;

  while (taskQueue.length > 0 && performance.now() < deadline) {
    const task = taskQueue.shift();
    task.callback();
  }

  if (taskQueue.length > 0) {
    port.postMessage(null); // 还有任务,排到下一个宏任务
  }
};

// 触发调度
port.postMessage(null);

为什么用 MessageChannel 而不是 setTimeout?因为 setTimeout(fn, 0) 在嵌套调用时会被浏览器强制降级为至少 4ms 的延迟,而 MessageChannel 没有这个限制,调度精度更高。

优先级:不是所有更新都同等重要

说白了,优先级调度就是让紧急的事先做,不急的事排队等。React 18 引入了 Lane 模型来管理优先级——每个更新被分配到一条”车道”,不同车道有不同的紧急程度。

用户正在输入框里打字,这个更新走 SyncLane(最高优先级,同步执行)。点击按钮触发的数据加载走 DefaultLane(普通优先级)。useTransition 包裹的更新走 TransitionLane(可以被打断的低优先级)。

当一个低优先级的 Render 正在进行,突然来了一个高优先级更新——Scheduler 会中断当前工作,优先处理紧急任务。处理完之后,低优先级的 Render 阶段不是从断点恢复,而是从头开始。这也是为什么 Render 阶段”没有副作用”如此重要:重新执行不会产生任何不良后果。

五、双缓冲:为什么需要两棵树

Fiber 架构在内存中始终维护两棵 Fiber 树。

Current Tree 对应当前屏幕上显示的 UI。Work-in-Progress Tree(WIP Tree) 是 Render 阶段正在构建的新树。两棵树的根节点通过 alternate 指针互相引用。

currentFiber.alternate === wipFiber; // true
wipFiber.alternate === currentFiber; // true

Render 阶段的所有计算都在 WIP Tree 上进行——对比新旧 props、标记变更、构建新的子树。整个过程不影响 Current Tree,因此屏幕上的 UI 保持稳定。

当 Render 阶段完成、进入 Commit 阶段时,React 把 WIP Tree 一次性”翻转”为 Current Tree(只需要改一个根指针的指向)。原来的 Current Tree 变成了下一轮更新的 WIP Tree 的起点,节点可以被复用,避免反复创建和销毁对象。

这就是”双缓冲”这个名字的由来——和显卡渲染的双缓冲原理一样:一个缓冲区用于显示,另一个用于绘制,绘制完毕后整体交换,用户永远看不到绘制过程中的中间状态。

六、这意味着什么

Fiber 架构本身不会让你的 React 应用自动变快。它提供的是一种能力:让 React 有权决定什么时候渲染、先渲染什么、要不要中断正在进行的渲染。

React 18 的所有并发特性都建立在这个能力之上。useTransition 能把一次状态更新标记为”不紧急”,让输入框保持流畅响应,因为 Fiber 的 Scheduler 知道如何区分优先级。useDeferredValue 能延迟一个值的更新,因为 Fiber 的两阶段模型允许 Render 被中断和重启。Suspense 能在数据加载时显示 fallback,因为 Fiber 树可以”暂挂”一个分支的渲染。

顺带提一下 Vue 的思路差异。Vue 选择了另一条路:通过编译时分析模板找出静态和动态部分,再配合响应式系统精确追踪到具体哪个数据变了、影响了哪些组件。Vue 的策略是让更新范围尽量小,React 的策略是让更新过程可以被管理。 两种思路在不同场景各有优势,但 Fiber 架构赋予 React 的运行时调度能力,是 Vue 的编译时优化不容易覆盖的领域。

七、总结

Fiber 解决的核心问题是:把同步、不可中断的递归渲染,改造成异步、可中断、有优先级的链表遍历。

这套改造包含三层:数据结构层(Fiber 节点 + 链表指针),执行层(两阶段模型 + 双缓冲),调度层(Scheduler + Lane 优先级)。三层配合,让 React 获得了”可商量”的渲染能力——不是一口气干完,而是根据优先级和时间预算,合理安排工作节奏。

理解了 Fiber,再去看 useTransitionSuspensestartTransition 这些 API,就不再是记用法,而是理解了它们为什么存在、为什么这样设计。


本系列其他文章:

相关主题:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;