Skip to main content

Fiber 架构

在 React 16 之前,更新虚拟 DOM 的过程称为 Stack Reconciler,这是一个递归的过程,在层级很深的时候,单次 diff 时间过长会让 JS 线程持续被占用,导致用户交互响应迟滞,页面渲染会出现明显的卡顿。

为了解决这种问题,React 团队基于 Fiber 架构重构了 React,使其能够将任务分片,划分优先级,同时能够实现类似于操作系统中对线程的抢占式调度。

面试必考:什么是 Fiber?为什么要引入 Fiber?

  1. 架构层面:Fiber 是 React 16 中新的协调引擎(Reconciler)。
  2. 数据结构层面:Fiber 是一个虚拟 DOM 节点,包含了组件的类型、DOM 节点信息、指针(指向父、子、兄弟节点,形成链表)。
  3. 动态工作单元层面:Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作(被标记的 EffectTag)。

为什么引入?:为了解决 React 15 以前的“同步递归渲染”导致主线程阻塞的问题,实现了异步可中断的更新。


1. 浏览器的渲染帧与卡顿原因

由于主流的屏幕刷新率都在 60Hz,因此浏览器渲染一帧的时间必须控制在 16.7ms 内才能保证不掉帧。

浏览器每一帧都需要完成哪些工作? 浏览器一帧内的工作.png

  1. 处理用户交互
  2. 解析 js 脚本
  3. Begin frame。resize、scroll 等事件的处理
  4. rAF (requestAnimationFrame)
  5. 布局 (Layout)
  6. 绘制 (Paint)

当浏览器在一帧内(16.6ms)执行完上述任务后,如果还有剩余时间,这段时间被称为空闲期(Idle Period)。React Fiber 的底层调度器(Scheduler)就是利用了这种机制(React 内部自己实现了一个 requestIdleCallback 的 Polyfill,基于 MessageChannel)在空闲期执行被拆分的渲染任务。


2. Fiber Reconciler 的核心机制

Fiber 协调核心在于:可中断可恢复优先级

新特性如下:

  • 给不同类型的更新任务赋予优先级,能够暂停、终止和复用渲染任务。
  • 把渲染任务拆分成块,匀到多帧。

将以前的 stack reconciler 拆分成两个阶段:Render 阶段Commit 阶段

  • Render 阶段:在这个阶段会进行 VDOM 的 Diff 计算,找出需要更新的节点。这个过程是可被打断的,被拆分成一个个的小任务(Fiber),在每一帧的空闲期执行。
  • Commit 阶段:这个阶段将 Render 阶段计算出的结果(effectList)应用到真实的 DOM 上。这个过程是同步执行、不可打断的,保证用户看到的 UI 是一致的。

优先级抢占逻辑

  • 每个更新任务都会赋予一个优先级(Lane 模型)。
  • 当高优先级的更新任务(B)抵达调度器时,如果当前正在执行低优先级任务(A),处于当前 Reconciler 层的 A 任务会被中断。
  • 当 B 任务完成渲染后,新一轮调度开始,之前被中断的 A 任务将会被重新推入 Reconciler 层,继续它的渲染(可恢复)。

3. Fiber Node 的数据结构

React Fiber 把更新过程碎片化,以 Fiber Node 为一个工作单元。

一个虚拟 DOM 节点对应一个 Fiber Node。为了实现可中断和恢复,Fiber 将树形结构改造成了单链表结构

type Fiber = {
// === 基础信息 ===
tag: WorkTag; // 标识不同的组件类型 (FunctionComponent, ClassComponent...)
key: null | string;
elementType: any; // react 元素类型
type: any; // 组件函数或类
stateNode: any; // 该 fiber 节点对应的真实 DOM 节点或组件实例

// === 链表结构指针 ===
return: Fiber | null; // 指向父节点
child: Fiber | null; // 指向第一个子节点
sibling: Fiber | null; // 指向下一个兄弟节点
index: number;

// === 工作状态 ===
pendingProps: any; // 正在计算的新 props
memoizedProps: any; // 上次渲染的旧 props
updateQueue: UpdateQueue<any> | null; // 状态更新队列
memoizedState: any; // 也就是 Hooks 链表存放的地方

// === 副作用 (DOM diff相关) ===
flags: Flags; // (以前叫 effectTag) 记录这个节点需要执行的操作:如新增、删除、更新
nextEffect: Fiber | null; // 指向下一个有副作用的 fiber

// === 双缓冲机制 ===
alternate: Fiber | null; // 指向 current 树中的对应节点
};

面试考点:为什么 Fiber 要采用单链表结构(return, child, sibling)而不是传统的树递归? 传统的树递归一旦开始就无法暂停(调用栈由 JavaScript 引擎控制,无法主动保存当前执行到哪一个节点)。单链表结构使得 React 可以在任意一个 Fiber 节点暂停工作,把控制权交还给浏览器,并且在恢复时通过 return 向上找到父节点,或者通过 sibling 找到兄弟节点,完美实现了渲染状态的保存和恢复。

4. 双缓冲机制 (Double Buffering)

这也是 Fiber 架构中的高级考点。

React 内部同时存在两棵 Fiber 树:

  • current:对应当前屏幕上显示内容的 Fiber 树。
  • workInProgress:正在内存中构建的 Fiber 树。

工作流: 当有更新发生时,React 会在内存中创建一棵新的 workInProgress 树。所有的 Diff 和 DOM 变更标记都是在这棵新树上进行的,这个过程可中断。当新树构建完成并且进入 Commit 阶段应用到真实 DOM 后,workInProgress 树就会通过一根指针(alternate)替换为新的 current 树。

好处: 这就像计算机图形学中的“双缓冲技术”,如果在直接显示在屏幕上的画布上作画,用户会看到撕裂和闪烁。而在内存中的隐藏画布(workInProgress)上作画,画完后瞬间替换(Commit 阶段),保证了用户看到的 UI 永远是完整和一致的,并且两棵树的节点可以通过 alternate 属性相互复用,极大节省了内存分配开销。

补充:早期 React(16/17)用 expirationTime(过期时间)表示优先级,React 18 起改为更灵活的 Lane(车道)模型,用二进制位表示多个并存的优先级。

Fiber 树示意图如下:

fiber树结构.png

创建fiber.gif

面试题:双缓冲树机制带来的三大优势是什么?

  1. 实现无缝更新:所有的 Diff 和状态计算都在内存中的 workInProgress 树进行,等全部计算完毕并在 Commit 阶段挂载到 DOM 后,再瞬间替换指针。避免了因为中断机制导致屏幕上出现“渲染一半”的半成品 UI。
  2. 极大的性能复用workInProgress 树节点在创建时,会尽可能复用 current 树节点中的属性,极大减少了对象的频繁创建和垃圾回收 (GC) 的时间开销。
  3. 异常隔离兜底:如果 workInProgress 树在 Render 阶段计算时发生严重报错,React 可以直接丢弃这棵树。由于真实的 DOM 依然由 current 树控制,页面不会白屏崩溃,只是这次更新失败了而已。