Fiber 架构
问题
什么是 React Fiber?为什么需要 Fiber 架构?什么是时间切片?
答案
Fiber 是 React 16 引入的新协调(Reconciliation)引擎。它的核心目标是实现可中断渲染,让浏览器在渲染大型组件树时仍能保持响应。
为什么需要 Fiber?
React 15 的问题
在 React 15 中,协调过程是同步递归的(Stack Reconciler):
// React 15 的递归渲染(简化)
function reconcileChildren(element) {
// 同步递归,无法中断
for (const child of element.children) {
reconcileChildren(child); // 递归调用
}
}
问题:
- 一旦开始渲染,就无法中断
- 大型组件树会占用主线程很长时间
- 用户交互(点击、输入)无法及时响应
- 页面出现卡顿
Fiber 的解决方案
Fiber 将渲染工作拆分成小单元,每个单元执行完后,检查是否有更高优先级的任务(如用户交互),有则暂停渲染,处理高优先级任务。
Fiber 是什么?
Fiber 的三层含义
| 层面 | 含义 |
|---|---|
| 架构 | 新的协调算法,支持可中断渲染 |
| 数据结构 | 每个组件对应一个 Fiber 节点 |
| 工作单元 | 每个 Fiber 节点是一个最小工作单元 |
Fiber 节点结构
interface FiberNode {
// 静态数据结构(描述组件)
tag: WorkTag; // 组件类型(函数组件、类组件、原生标签等)
type: any; // 对应的 React 元素类型
key: string | null; // key 属性
// 链接其他 Fiber(形成树结构)
return: FiberNode | null; // 父节点
child: FiberNode | null; // 第一个子节点
sibling: FiberNode | null; // 下一个兄弟节点
// 动态工作单元(状态相关)
pendingProps: any; // 新的 props
memoizedProps: any; // 上次渲染的 props
memoizedState: any; // 上次渲染的 state(或 Hooks 链表)
// 副作用
flags: Flags; // 副作用标记(插入、更新、删除等)
subtreeFlags: Flags; // 子树的副作用标记
// 调度优先级
lanes: Lanes; // 优先级
childLanes: Lanes; // 子树的优先级
// 双缓存
alternate: FiberNode | null; // 指向另一棵树中对应的 Fiber
}
Fiber 树结构
Fiber 树使用三个指针连接:
child:指向第一个子节点sibling:指向下一个兄弟节点return:指向父节点
这种结构使得遍历可以随时暂停和恢复。
双缓存机制
React 同时维护两棵 Fiber 树:
| 树 | 说明 |
|---|---|
| current 树 | 当前屏幕上显示的内容对应的 Fiber 树 |
| workInProgress 树 | 正在内存中构建的新 Fiber 树 |
双缓存工作流程
优势:
- 构建过程中不影响当前显示
- 如果构建被中断,当前界面不受影响
- 构建完成后一次性切换,避免闪烁
时间切片(Time Slicing)
什么是时间切片?
时间切片是将长任务拆分成多个小任务,每个小任务执行完后,让出主线程给浏览器处理更重要的事情。
// 传统方式:同步执行,阻塞主线程
function renderAll(units: WorkUnit[]): void {
for (const unit of units) {
processUnit(unit); // 可能很耗时
}
}
// 时间切片方式:分批执行,定期让出主线程
function workLoop(deadline: IdleDeadline): void {
while (workInProgress && deadline.timeRemaining() > 0) {
workInProgress = performUnitOfWork(workInProgress);
}
if (workInProgress) {
// 还有工作,请求下一次调度
requestIdleCallback(workLoop);
}
}
调度器(Scheduler)
React 有自己的调度器,使用 MessageChannel 实现(不用 requestIdleCallback 是因为兼容性问题):
// React 调度器简化实现
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
function scheduleCallback(callback: () => void): void {
taskQueue.push(callback);
port.postMessage(null);
}
function performWorkUntilDeadline(): void {
const currentTime = performance.now();
const deadline = currentTime + 5; // 5ms 时间片
while (taskQueue.length > 0 && performance.now() < deadline) {
const task = taskQueue.shift();
task?.();
}
if (taskQueue.length > 0) {
port.postMessage(null); // 继续调度
}
}
优先级调度
React 18 使用 Lanes 模型管理优先级:
// 优先级从高到低
const SyncLane = 0b0001; // 同步(最高,用户输入)
const InputContinuousLane = 0b0010; // 连续输入(拖拽、滚动)
const DefaultLane = 0b0100; // 默认(普通更新)
const IdleLane = 0b1000; // 空闲(最低,如预渲染)
可中断渲染过程
// 工作循环(简化)
function workLoopConcurrent(): void {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
// 是否应该让出主线程
function shouldYield(): boolean {
const currentTime = performance.now();
return currentTime >= deadline;
}
// 执行单个工作单元
function performUnitOfWork(unitOfWork: FiberNode): FiberNode | null {
// 1. 处理当前 Fiber(beginWork)
const next = beginWork(unitOfWork);
if (next !== null) {
// 有子节点,继续处理子节点
return next;
}
// 2. 没有子节点,完成当前 Fiber(completeWork)
return completeUnitOfWork(unitOfWork);
}
Fiber 工作流程
完整流程图
beginWork 阶段
function beginWork(
current: FiberNode | null,
workInProgress: FiberNode
): FiberNode | null {
// 根据 Fiber 类型处理
switch (workInProgress.tag) {
case FunctionComponent:
return updateFunctionComponent(current, workInProgress);
case ClassComponent:
return updateClassComponent(current, workInProgress);
case HostComponent: // div、span 等原生标签
return updateHostComponent(current, workInProgress);
// ... 其他类型
}
}
function updateFunctionComponent(
current: FiberNode | null,
workInProgress: FiberNode
): FiberNode | null {
// 执行函数组件,获取子元素
const children = renderWithHooks(workInProgress);
// 协调子节点(Diff)
reconcileChildren(current, workInProgress, children);
return workInProgress.child;
}
completeWork 阶段
function completeWork(
current: FiberNode | null,
workInProgress: FiberNode
): void {
switch (workInProgress.tag) {
case HostComponent:
if (current !== null) {
// 更新:比较 props,收集更新
updateHostComponent(current, workInProgress);
} else {
// 新建:创建 DOM 节点
const instance = createInstance(workInProgress.type);
appendAllChildren(instance, workInProgress);
workInProgress.stateNode = instance;
}
break;
// ... 其他类型
}
// 冒泡副作用标记
bubbleProperties(workInProgress);
}
副作用收集
Fiber 使用 flags 标记副作用,在 Commit 阶段统一处理:
// 副作用标记
const NoFlags = 0b00000000;
const Placement = 0b00000001; // 插入
const Update = 0b00000010; // 更新
const Deletion = 0b00000100; // 删除
const ChildDeletion = 0b00001000;
常见面试问题
Q1: 什么是 React Fiber?解决了什么问题?
答案:
Fiber 是 React 16 引入的新协调引擎,解决了 React 15 的同步渲染阻塞问题。
| React 15 | React 16+ (Fiber) |
|---|---|
| Stack Reconciler | Fiber Reconciler |
| 同步递归 | 可中断的循环 |
| 无法中断 | 可以暂停、恢复、放弃 |
| 长任务阻塞 | 时间切片,及时响应 |
Fiber 的三层含义:
- 架构:新的可中断协调算法
- 数据结构:每个组件对应一个 Fiber 节点
- 工作单元:最小的可执行单位
Q2: 什么是双缓存?为什么需要双缓存?
答案:
双缓存是同时维护两棵 Fiber 树:
| 树 | 作用 |
|---|---|
| current 树 | 当前屏幕显示的内容 |
| workInProgress 树 | 内存中正在构建的新内容 |
为什么需要:
- 不影响当前显示:构建过程中,用户看到的仍是 current 树
- 可中断:如果构建被中断,current 树不受影响
- 避免闪烁:构建完成后一次性切换,用户感知不到中间状态
- 复用节点:通过
alternate指针复用上次的 Fiber 节点
Q3: 什么是时间切片?React 是如何实现的?
答案:
时间切片是将长任务拆分成小任务,每个小任务执行后检查是否需要让出主线程。
实现原理:
// 简化实现
function workLoop(): void {
while (workInProgress && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
if (workInProgress) {
scheduleCallback(workLoop); // 下一帧继续
}
}
function shouldYield(): boolean {
return performance.now() >= deadline; // 超过 5ms
}
关键点:
- 每个时间片约 5ms
- 使用
MessageChannel实现调度(不用 rAF 是因为帧率不稳定) - 高优先级任务(如用户输入)可以打断低优先级任务
Q4: React 的优先级机制是怎样的?
答案:
React 18 使用 Lanes 模型管理优先级:
| 优先级 | 场景 | 特点 |
|---|---|---|
| SyncLane | 用户输入、点击 | 最高,同步执行 |
| InputContinuousLane | 拖拽、滚动 | 高,连续响应 |
| DefaultLane | 普通 setState | 默认 |
| TransitionLane | useTransition | 可被打断 |
| IdleLane | 预渲染、离屏渲染 | 最低,空闲执行 |
// 高优先级打断低优先级
const [isPending, startTransition] = useTransition();
function handleChange(e: ChangeEvent<HTMLInputElement>) {
// 高优先级:立即更新输入框
setInputValue(e.target.value);
// 低优先级:可被打断的搜索
startTransition(() => {
setSearchResults(search(e.target.value));
});
}
Q5: Render 阶段和 Commit 阶段的区别?
答案:
| 阶段 | Render 阶段 | Commit 阶段 |
|---|---|---|
| 可中断性 | ✅ 可中断 | ❌ 不可中断 |
| 主要工作 | 构建 Fiber 树、Diff、收集副作用 | 执行副作用、更新 DOM |
| 执行函数 | beginWork、completeWork | commitMutationEffects 等 |
| 副作用 | 只标记,不执行 | 执行副作用(DOM 操作、生命周期) |
| 与 DOM | 不触及 DOM | 操作 DOM |
Q6: Fiber 的时间切片(Time Slicing)是如何工作的?
答案:
时间切片的核心思想是将长时间的渲染任务拆分成多个小的工作单元,每执行完一个单元就检查是否需要让出主线程,从而保证浏览器有机会处理用户交互和页面渲染。
实现机制:
-
调度器使用 MessageChannel:React 没有使用
requestIdleCallback(因为兼容性差、触发频率不稳定),而是基于MessageChannel实现了自己的调度器。 -
5ms 时间片:每个时间片默认约 5ms,这个时间足够执行一些工作,又不会导致用户感知到卡顿。
-
shouldYield 判断:每处理完一个 Fiber 节点后,调用
shouldYield()检查是否超时。
// React Scheduler 简化实现
const yieldInterval = 5; // 5ms 时间片
let deadline = 0;
// 使用 MessageChannel 而非 requestIdleCallback
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = () => {
const currentTime = performance.now();
deadline = currentTime + yieldInterval;
// 执行任务队列中的任务
const hasMoreWork = flushWork(currentTime);
if (hasMoreWork) {
// 还有任务,继续调度
port.postMessage(null);
}
};
function shouldYield(): boolean {
const currentTime = performance.now();
// 超过 5ms 时间片,需要让出主线程
return currentTime >= deadline;
}
// 工作循环
function workLoopConcurrent(): void {
while (workInProgress !== null && !shouldYield()) {
// 执行一个工作单元
workInProgress = performUnitOfWork(workInProgress);
}
// 如果还有工作但需要让出,下一帧继续
}
// 请求调度
function scheduleWork(): void {
port.postMessage(null); // 触发 MessageChannel,下一个宏任务执行
}
工作流程:
- 兼容性差:Safari 不支持
- 触发频率不稳定:浏览器在高负载时可能很久不触发
- 无法控制时间片长度:React 需要精确控制每帧的执行时间
- FPS 限制:requestIdleCallback 只在帧末尾空闲时触发,最多 50ms 一次
Q7: Fiber 的优先级调度是怎么实现的?
答案:
React 18 使用 Lanes(车道) 模型来管理优先级。每个 Lane 是一个二进制位,可以方便地进行位运算来合并、比较优先级。
Lane 模型:
// Lane 定义(使用二进制位表示优先级)
type Lane = number;
type Lanes = number;
const NoLane: Lane = 0b0000000000000000000000000000000;
const SyncLane: Lane = 0b0000000000000000000000000000001; // 同步,最高优先级
const InputContinuousLane: Lane = 0b0000000000000000000000000000100; // 连续输入
const DefaultLane: Lane = 0b0000000000000000000000000010000; // 默认优先级
const TransitionLane1: Lane = 0b0000000000000000000000001000000; // Transition
const IdleLane: Lane = 0b0100000000000000000000000000000; // 空闲,最低优先级
// 位运算合并多个 Lane
function mergeLanes(a: Lanes, b: Lanes): Lanes {
return a | b;
}
// 判断是否包含某个 Lane
function includesLane(set: Lanes, lane: Lane): boolean {
return (set & lane) !== 0;
}
// 获取最高优先级的 Lane(最低位)
function getHighestPriorityLane(lanes: Lanes): Lane {
return lanes & -lanes;
}
不同操作对应的优先级:
| 优先级 | Lane | 场景 | 特点 |
|---|---|---|---|
| SyncLane | 最高位 | 用户点击、输入、focus | 同步执行,不可中断 |
| InputContinuousLane | 高 | 拖拽、滚动、mousemove | 连续响应,高频触发 |
| DefaultLane | 中 | 普通 setState、fetch 回调 | 默认优先级 |
| TransitionLane | 低 | useTransition、useDeferredValue | 可被打断 |
| IdleLane | 最低 | offscreen 预渲染 | 空闲时执行 |
优先级调度流程:
// 触发更新时,根据上下文分配 Lane
function requestUpdateLane(): Lane {
// 如果在离散事件中(click)→ SyncLane
// 如果在连续事件中(scroll)→ InputContinuousLane
// 如果在 transition 中 → TransitionLane
// 否则 → DefaultLane
if (isDiscreteEventContext) {
return SyncLane;
}
if (isContinuousEventContext) {
return InputContinuousLane;
}
if (currentTransition !== null) {
return claimNextTransitionLane();
}
return DefaultLane;
}
// 调度时选择最高优先级的 Lane 执行
function ensureRootIsScheduled(root: FiberRoot): void {
const nextLanes = getNextLanes(root);
const highestLane = getHighestPriorityLane(nextLanes);
if (highestLane === SyncLane) {
// 同步执行,不走 Scheduler
scheduleSyncCallback(performSyncWorkOnRoot);
} else {
// 根据优先级计算 Scheduler 优先级
const schedulerPriority = lanesToSchedulerPriority(highestLane);
scheduleCallback(schedulerPriority, performConcurrentWorkOnRoot);
}
}
饥饿问题处理:
低优先级任务可能一直被高优先级任务打断,导致永远无法执行(饥饿)。React 通过过期时间解决:
// 每个 Lane 有对应的过期时间
function markStarvedLanesAsExpired(root: FiberRoot, currentTime: number): void {
const pendingLanes = root.pendingLanes;
const expirationTimes = root.expirationTimes;
let lanes = pendingLanes;
while (lanes > 0) {
const lane = getHighestPriorityLane(lanes);
const expirationTime = expirationTimes[laneToIndex(lane)];
if (expirationTime <= currentTime) {
// 已过期,提升为同步优先级,确保立即执行
root.expiredLanes |= lane;
}
lanes &= ~lane; // 移除已处理的 lane
}
}
// 过期的 Lane 会被当作 SyncLane 处理,保证一定执行
Q8: 双缓冲(Double Buffering)在 Fiber 中是怎么工作的?
答案:
双缓冲是一种经典的图形渲染技术,React Fiber 借用这个概念来避免渲染过程中的 UI 不一致。React 同时维护两棵 Fiber 树,通过 alternate 指针互相连接。
两棵 Fiber 树:
| 树 | 作用 | 何时存在 |
|---|---|---|
| current 树 | 代表当前屏幕上显示的 UI | 始终存在 |
| workInProgress 树 | 正在内存中构建的新 UI | 渲染过程中存在 |
interface FiberNode {
// ...其他字段
alternate: FiberNode | null; // 指向另一棵树中对应的节点
stateNode: any; // 对应的真实 DOM 节点
}
// current 和 workInProgress 通过 alternate 互相引用
// currentFiber.alternate === workInProgressFiber
// workInProgressFiber.alternate === currentFiber
工作流程:
节点复用机制:
// 创建 workInProgress 节点时尝试复用
function createWorkInProgress(
current: FiberNode,
pendingProps: any
): FiberNode {
let workInProgress = current.alternate;
if (workInProgress === null) {
// 首次渲染,没有可复用的节点,创建新的
workInProgress = createFiber(current.tag, pendingProps, current.key);
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode; // 复用 DOM 节点
// 建立 alternate 连接
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// 更新渲染,复用已有节点,只更新属性
workInProgress.pendingProps = pendingProps;
workInProgress.flags = NoFlags; // 重置副作用
workInProgress.subtreeFlags = NoFlags;
}
// 复制静态属性
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.lanes = current.lanes;
return workInProgress;
}
Commit 阶段切换:
// Commit 完成后,交换 current 指针
function commitRoot(root: FiberRoot): void {
const finishedWork = root.finishedWork; // workInProgress 树的根节点
// 1. Before Mutation 阶段
commitBeforeMutationEffects(finishedWork);
// 2. Mutation 阶段:操作真实 DOM
commitMutationEffects(finishedWork);
// 3. 关键步骤:切换 current 指针
root.current = finishedWork;
// 此时 workInProgress 树变成了 current 树
// 旧的 current 树通过 alternate 保留,下次更新时作为 workInProgress 复用
// 4. Layout 阶段
commitLayoutEffects(finishedWork);
}
双缓冲的优势:
- 避免 UI 不一致:构建新树期间,屏幕上显示的始终是 current 树,用户不会看到中间状态
- 支持可中断渲染:如果渲染被中断,丢弃 workInProgress 树即可,current 树不受影响
- 节点复用:通过 alternate 指针,两棵树的节点可以互相复用,减少内存分配
- 一致性保证:所有 DOM 变更在 Commit 阶段一次性应用,然后切换 current 指针
双缓冲只在 Render 阶段体现可中断性。Commit 阶段是同步执行的,一旦开始就不会中断,这保证了 DOM 更新的原子性。