深入理解 Fiber 架构与调度器
Fiber 节点结构、双缓冲树、优先级车道(Lanes)、时间切片与 Scheduler 调度原理
什么是 Fiber 架构?
定义:Fiber 是 React 16 引入的全新协调引擎,将原来不可中断的递归渲染(Stack Reconciler)重构为可中断、可恢复的链表遍历(Fiber Reconciler)。每个 React 元素对应一个 Fiber 节点,所有 Fiber 节点通过 child、sibling、return 指针组成一棵树。
涉及场景:
- 长列表渲染:大量组件更新时不会阻塞用户输入
- 动画流畅性:高优先级的动画帧不被低优先级的数据更新阻塞
- 并发特性基础:useTransition、Suspense、流式 SSR 都依赖 Fiber 的可中断能力
- React DevTools:Profiler 展示的组件树就是 Fiber 树
作用:
- 可中断渲染:将渲染工作拆分为小单元,每个单元执行完可以让出主线程
- 优先级调度:不同类型的更新有不同优先级,紧急更新优先处理
- 增量渲染:Render 阶段可中断,Commit 阶段不可中断
- 面试重点:Fiber 架构是理解 React 并发特性的基石
一、Fiber 节点结构
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 指向父节点遍历顺序(深度优先):
- 从当前节点开始,先处理
child - 没有 child 时,处理
sibling - 没有 sibling 时,回到
return(父节点),找父节点的 sibling - 直到回到根节点
二、双缓冲(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。
// 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 中的 setState | SyncLane | 最高 |
| 连续输入(拖拽、滚动) | InputContinuousLane | 高 |
useEffect 中的 setState | DefaultLane | 默认 |
startTransition(() => setState()) | TransitionLane | 低 |
Suspense 显示 fallback | RetryLane | 低 |
Lane 的位运算
// 合并多个 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)
// 简化的工作循环
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 的任务优先级
// 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 工作
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)。
// 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 条件:
oldProps === newProps(引用比较)- Context 未变化
- 当前 Fiber 没有待处理的更新(lanes 检查)
- 组件类型未变化
这就是为什么 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 负责实际的任务排序和时间切片。