Skip to content

深入理解 Fiber 架构与调度器

Fiber 节点结构、双缓冲树、优先级车道(Lanes)、时间切片与 Scheduler 调度原理

什么是 Fiber 架构?

定义:Fiber 是 React 16 引入的全新协调引擎,将原来不可中断的递归渲染(Stack Reconciler)重构为可中断、可恢复的链表遍历(Fiber Reconciler)。每个 React 元素对应一个 Fiber 节点,所有 Fiber 节点通过 childsiblingreturn 指针组成一棵树。

涉及场景

  • 长列表渲染:大量组件更新时不会阻塞用户输入
  • 动画流畅性:高优先级的动画帧不被低优先级的数据更新阻塞
  • 并发特性基础:useTransition、Suspense、流式 SSR 都依赖 Fiber 的可中断能力
  • React DevTools:Profiler 展示的组件树就是 Fiber 树

作用

  1. 可中断渲染:将渲染工作拆分为小单元,每个单元执行完可以让出主线程
  2. 优先级调度:不同类型的更新有不同优先级,紧急更新优先处理
  3. 增量渲染:Render 阶段可中断,Commit 阶段不可中断
  4. 面试重点:Fiber 架构是理解 React 并发特性的基石

一、Fiber 节点结构

typescript
interface FiberNode {
  // === 静态结构 ===
  tag: WorkTag;              // 组件类型标记(FunctionComponent=0, ClassComponent=1, HostComponent=5...)
  type: any;                 // 函数组件是函数本身,DOM 元素是标签名字符串
  key: string | null;        // 用于 Diff 的 key
  elementType: any;          // 大多数情况与 type 相同

  // === 实例相关 ===
  stateNode: any;            // DOM 节点 | 类组件实例 | null(函数组件)

  // === Fiber 树结构(链表指针)===
  return: FiberNode | null;  // 父节点
  child: FiberNode | null;   // 第一个子节点
  sibling: FiberNode | null; // 下一个兄弟节点
  index: number;             // 在父节点中的位置索引

  // === 工作单元 ===
  pendingProps: any;         // 本次渲染待处理的 props
  memoizedProps: any;        // 上次渲染完成的 props
  memoizedState: any;        // 上次渲染完成的 state(Hooks 链表头)
  updateQueue: any;          // 状态更新队列

  // === 副作用 ===
  flags: Flags;              // 副作用标记(Placement=2, Update=4, Deletion=8...)
  subtreeFlags: Flags;       // 子树副作用标记(冒泡收集)
  deletions: FiberNode[];    // 待删除的子节点

  // === 双缓冲 ===
  alternate: FiberNode | null; // 指向另一棵树的对应节点

  // === 优先级 ===
  lanes: Lanes;              // 本节点的优先级
  childLanes: Lanes;         // 子树的优先级
}

Fiber 树的链表遍历

        App
       / 
      div
     /   \
   Header  Main
   /        /  \
  h1    Article Sidebar
          |
          p

链表指针:
App.child → div
div.child → Header       Header.sibling → Main
Header.child → h1        Main.child → Article
                          Article.sibling → Sidebar
                          Article.child → p

所有节点的 return 指向父节点

遍历顺序(深度优先):

  1. 从当前节点开始,先处理 child
  2. 没有 child 时,处理 sibling
  3. 没有 sibling 时,回到 return(父节点),找父节点的 sibling
  4. 直到回到根节点

二、双缓冲(Double Buffering)

React 同时维护两棵 Fiber 树:

Current Tree(当前屏幕显示)          WorkInProgress Tree(正在构建)
┌──────────┐                        ┌──────────┐
│  App     │ ←── alternate ────→    │  App     │
│ current  │                        │   wip    │
└──────────┘                        └──────────┘
     ↓ child                             ↓ child
┌──────────┐                        ┌──────────┐
│  div     │ ←── alternate ────→    │  div     │
│ current  │                        │   wip    │
└──────────┘                        └──────────┘

更新流程:
1. 触发更新 → 基于 Current Tree 创建 WorkInProgress Tree
2. 在 WIP 树上完成所有计算(可中断)
3. Commit 阶段:将 WIP 树一次性提交为新的 Current Tree
4. 原来的 Current Tree 变为下次更新的 WIP 树基础(复用节点)

优势

  • 无闪烁:所有计算在内存中完成,只在 Commit 时一次性更新 DOM
  • 可中断:WIP 树构建过程可以被高优先级更新打断并丢弃
  • 内存复用:两棵树通过 alternate 互相引用,节点对象被复用

三、渲染流程(两个阶段)

Render 阶段(可中断)

beginWork(递):从根节点向下,为每个 Fiber 创建/复用子 Fiber

  ├── 根据 tag 执行不同逻辑:
  │   ├── FunctionComponent → 执行函数,调用 Hooks
  │   ├── ClassComponent → 调用 render()
  │   ├── HostComponent → 处理 DOM 属性
  │   └── ...

  ├── Diff 算法:对比新旧子节点
  │   ├── 可复用 → 标记 Update
  │   ├── 需新建 → 标记 Placement
  │   └── 需删除 → 标记 Deletion(加入 deletions 数组)

  └── 返回 child(继续向下)或 null(触发 completeWork)

completeWork(归):从叶子节点向上,创建/更新 DOM 节点

  ├── HostComponent → 创建 DOM 节点 / 计算属性 diff
  ├── 收集副作用:subtreeFlags |= child.flags | child.subtreeFlags
  └── 返回 sibling 或 return(继续向上)

Commit 阶段(不可中断)

1. BeforeMutation 阶段
   ├── 执行 getSnapshotBeforeUpdate(类组件)
   └── 调度 useEffect 的异步回调

2. Mutation 阶段(操作 DOM)
   ├── Placement → appendChild / insertBefore
   ├── Update → 更新 DOM 属性
   ├── Deletion → removeChild + 清理(ref、useEffect 清理函数)
   └── 切换 Current Tree(fiberRoot.current = wip)

3. Layout 阶段
   ├── 执行 useLayoutEffect 回调
   ├── 更新 ref
   └── 执行 componentDidMount / componentDidUpdate

4. 异步调度
   └── 执行 useEffect 回调(通过 Scheduler 异步调度)

四、优先级系统(Lanes 模型)

React 18 使用 Lanes(车道)模型管理优先级,每个 Lane 是一个 32 位整数中的一个 bit。

typescript
// Lane 优先级定义(部分)
const SyncLane          = 0b0000000000000000000000000000010;  // 同步(最高)
const InputContinuous   = 0b0000000000000000000000000001000;  // 连续输入(拖拽)
const DefaultLane       = 0b0000000000000000000000000100000;  // 默认(数据请求)
const TransitionLane1   = 0b0000000000000000000001000000000;  // Transition
const IdleLane          = 0b0100000000000000000000000000000;  // 空闲

// 优先级从高到低:
// Sync > InputContinuous > Default > Transition > Idle

不同操作的优先级

操作Lane优先级
flushSync(() => setState())SyncLane最高(同步执行)
onClick 中的 setStateSyncLane最高
连续输入(拖拽、滚动)InputContinuousLane
useEffect 中的 setStateDefaultLane默认
startTransition(() => setState())TransitionLane
Suspense 显示 fallbackRetryLane

Lane 的位运算

javascript
// 合并多个 Lane
const merged = laneA | laneB;

// 判断是否包含某个 Lane
const includes = (set & subset) === subset;

// 移除某个 Lane
const remaining = set & ~lane;

// 获取最高优先级 Lane
const highest = lanes & -lanes; // 取最低位的 1

五、Scheduler(调度器)

Scheduler 是 React 的独立包(scheduler),负责任务的优先级调度和时间切片。

时间切片(Time Slicing)

javascript
// 简化的工作循环
function workLoop(deadline) {
  let shouldYield = false;

  while (workInProgress !== null && !shouldYield) {
    // 处理一个 Fiber 节点
    workInProgress = performUnitOfWork(workInProgress);
    // 检查是否需要让出主线程
    shouldYield = deadline.timeRemaining() < 5; // 剩余时间不足 5ms
  }

  if (workInProgress !== null) {
    // 还有未完成的工作,申请下一个时间片
    requestIdleCallback(workLoop); // 实际用 MessageChannel
  } else {
    // 所有工作完成,进入 Commit 阶段
    commitRoot();
  }
}

Scheduler 的任务优先级

javascript
// Scheduler 内部的5种优先级
const ImmediatePriority = 1;   // 超时时间: -1ms(立即过期)
const UserBlockingPriority = 2; // 超时时间: 250ms
const NormalPriority = 3;       // 超时时间: 5000ms
const LowPriority = 4;          // 超时时间: 10000ms
const IdlePriority = 5;         // 超时时间: maxSigned31BitInt

// 任务调度
function scheduleCallback(priority, callback) {
  const currentTime = getCurrentTime();
  const timeout = priorityToTimeout(priority);
  const expirationTime = currentTime + timeout;

  const task = {
    callback,
    priorityLevel: priority,
    expirationTime,
    sortIndex: expirationTime, // 小顶堆排序依据
  };

  // 加入任务队列(小顶堆),按 expirationTime 排序
  push(taskQueue, task);
  // 通过 MessageChannel 调度执行
  schedulePerformWorkUntilDeadline();
}

MessageChannel vs requestIdleCallback

React 实际使用 MessageChannel 而非 requestIdleCallback

  • requestIdleCallback 只在浏览器空闲时执行,频率不可控(可能 20fps)
  • MessageChannel 的回调在每一帧的微任务之后、绘制之前执行
  • 手动控制 5ms 的时间片,保证 每帧都有机会执行 React 工作
javascript
const channel = new MessageChannel();
const port = channel.port2;

channel.port1.onmessage = performWorkUntilDeadline;

function schedulePerformWorkUntilDeadline() {
  port.postMessage(null); // 触发下一个任务
}

六、更新流程全貌

setState() / dispatch()

创建 Update 对象,加入 Fiber 节点的 updateQueue

根据触发场景确定 Lane 优先级

从触发节点向上标记到根节点(markUpdateLaneFromFiberToRoot)

在根节点上调度更新(ensureRootIsScheduled)

Scheduler 按优先级调度任务

进入 Render 阶段
  ├── beginWork(递):执行组件函数/render,Diff 子节点
  └── completeWork(归):创建/更新 DOM,收集副作用

进入 Commit 阶段
  ├── BeforeMutation → Mutation → Layout
  └── 异步调度 useEffect

fiberRoot.current 指向新树

七、Bailout 优化

当 React 判断组件不需要更新时,会跳过该组件及其子树的渲染(bailout)。

javascript
// beginWork 中的 bailout 判断
function beginWork(current, workInProgress, renderLanes) {
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps === newProps &&             // props 引用相同
      !hasContextChanged() &&              // Context 未变化
      (current.lanes & renderLanes) === 0  // 没有待处理的更新
    ) {
      // Bailout:跳过该节点
      // 检查子树是否有更新
      if ((current.childLanes & renderLanes) === 0) {
        return null; // 子树也没更新,直接跳过整棵子树
      }
      // 子树有更新,克隆子节点继续
      return cloneChildFibers(current, workInProgress);
    }
  }
  // 需要更新,执行组件逻辑...
}

bailout 条件

  1. oldProps === newProps(引用比较)
  2. Context 未变化
  3. 当前 Fiber 没有待处理的更新(lanes 检查)
  4. 组件类型未变化

这就是为什么 React.memo 和 React Compiler 要确保 props 引用稳定——只有引用不变才能触发 bailout。


面试高频题

Q: React 为什么从 Stack 架构换成 Fiber 架构?

Stack Reconciler 使用递归遍历组件树,一旦开始就无法中断。大组件树的更新可能占用主线程几十甚至上百毫秒,导致页面卡顿。Fiber 将工作拆分为小单元,每个单元执行完可以检查是否需要让出主线程,实现可中断的增量渲染

Q: Render 阶段为什么可以中断,Commit 阶段为什么不能?

  • Render 阶段是在内存中构建 WIP 树,不涉及 DOM 操作,中断后可以丢弃或恢复
  • Commit 阶段直接操作 DOM,如果中断会导致 DOM 处于不一致状态(用户看到半更新的界面)

Q: React 的时间切片是怎么实现的?

通过 MessageChannel 在每一帧中获取执行时机,每次执行控制在 5ms 以内。超时后让出主线程,通过 port.postMessage 申请下一个时间片。不使用 requestIdleCallback 是因为其执行频率不可控。

Q: Lanes 和 Scheduler 的优先级是什么关系?

Lanes 是 React 内部的更新优先级模型(32位),Scheduler 是通用任务调度器(5种优先级)。React 在调度更新时,将 Lane 映射为 Scheduler 的优先级,由 Scheduler 负责实际的任务排序和时间切片。