Fiber 架构与 Diff 算法
1. 为什么需要 Fiber?
1.1 Stack Reconciler 的问题 (React 15)
┌─────────────────────────────────────────────────────────────────────┐
│ Stack Reconciler (React 15) │
├─────────────────────────────────────────────────────────────────────┤
│ render() → 递归遍历整棵树 → 同步更新 DOM │
│ │
│ 组件树深度大时: │
│ ┌────────┬────────┬────────┬────────┬────────┐ │
│ │ JS执行 │ JS执行 │ JS执行 │ JS执行 │ 渲染 │ │
│ │ 16ms+ │ 16ms+ │ 16ms+ │ 16ms+ │ │ │
│ └────────┴────────┴────────┴────────┴────────┘ │
│ ↑ │
│ 掉帧、卡顿! (超过 16.6ms/帧) │
└─────────────────────────────────────────────────────────────────────┘1.2 Fiber Reconciler 的解决方案 (React 16+)
┌─────────────────────────────────────────────────────────────────────┐
│ Fiber Reconciler (React 16+) │
├─────────────────────────────────────────────────────────────────────┤
│ Time Slicing (时间切片): │
│ ┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐ │
│ │Work││渲染││Work││渲染││Work││渲染││Work││渲染│ │
│ │ 5ms││ ││ 5ms││ ││ 5ms││ ││ 5ms││ │ │
│ └────┘└────┘└────┘└────┘└────┘└────┘└────┘└────┘ │
│ ↑ │
│ 可中断/恢复, 每帧预留时间给浏览器渲染 │
└─────────────────────────────────────────────────────────────────────┘| 特性 | Stack Reconciler | Fiber Reconciler |
|---|---|---|
| 更新方式 | 同步递归 | 异步可中断 |
| 数据结构 | 调用栈 | 链表 |
| 优先级 | 无 | 支持优先级调度 |
| 中断恢复 | 不支持 | 支持 |
2. Fiber 结构 🔥
2.1 什么是 Fiber?
Fiber 有三层含义:
- 架构角度: 新的 Reconciler 架构
- 数据结构: 虚拟 DOM 的增强版,也是工作单元
- 工作单元: 一个 Fiber 节点对应一次增量渲染任务
2.2 Fiber 节点结构
javascript
function FiberNode(tag, pendingProps, key, mode) {
// ============ 静态数据 (描述组件) ============
this.tag = tag; // 组件类型 (FunctionComponent, ClassComponent, HostComponent...)
this.key = key; // key 属性
this.elementType = null; // 元素类型 (div, span, MyComponent)
this.type = null; // 组件函数/类
this.stateNode = null; // 对应的真实 DOM / 类实例
// ============ Fiber 链表结构 ============
this.return = null; // 父 Fiber
this.child = null; // 第一个子 Fiber
this.sibling = null; // 下一个兄弟 Fiber
this.index = 0; // 在兄弟中的索引
// ============ 工作单元数据 ============
this.pendingProps = null; // 新 props
this.memoizedProps = null; // 上次渲染的 props
this.memoizedState = null; // 上次渲染的 state (Hooks 链表头)
this.updateQueue = null; // 更新队列
// ============ 副作用 ============
this.flags = NoFlags; // 副作用标记 (Placement, Update, Deletion...)
this.subtreeFlags = NoFlags; // 子树副作用
this.deletions = null; // 待删除的子节点
// ============ 优先级调度 ============
this.lanes = NoLanes; // 本节点优先级
this.childLanes = NoLanes; // 子树优先级
// ============ 双缓冲 ============
this.alternate = null; // 连接 current 和 workInProgress
}2.3 Fiber 链表结构图解
┌────────────┐
│ App │ (root)
└─────┬──────┘
│ child
┌─────▼──────┐
return ←──────│ Header │
└─────┬──────┘
│ sibling
┌─────▼──────┐
return ←──────│ Content │──────→ sibling
└─────┬──────┘ (Footer)
│ child
┌─────▼──────┐
return ←──────│ Title │────→ sibling (List)
└────────────┘2.4 关键字段详解
| 字段 | 类型 | 说明 |
|---|---|---|
tag | number | Fiber 类型 (见 WorkTag) |
type | Function/Class/string | 组件函数、类或原生标签名 |
stateNode | DOM/Instance | 对应的 DOM 节点或类实例 |
memoizedState | any | Function 组件的 Hooks 链表 |
flags | number | 副作用标记 (位运算) |
alternate | Fiber | 双缓冲对应节点 |
lanes | number | 优先级 (Lane 模型) |
2.5 WorkTag 类型
javascript
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // 首次渲染前不确定
export const HostRoot = 3; // 根节点
export const HostPortal = 4; // Portal
export const HostComponent = 5; // 原生 DOM (div, span)
export const HostText = 6; // 文本节点
export const Fragment = 7;
export const ContextProvider = 10;
export const ContextConsumer = 11;
export const ForwardRef = 12;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const SuspenseComponent = 13;
// ...2.6 Effect Flags (副作用标记)
javascript
export const NoFlags = 0b0000000000000000000;
export const Placement = 0b0000000000000000010; // 插入
export const Update = 0b0000000000000000100; // 更新
export const Deletion = 0b0000000000000001000; // 删除
export const ChildDeletion = 0b0000000000000010000; // 子节点删除
export const ContentReset = 0b0000000000000100000; // 重置内容
export const Callback = 0b0000000000001000000; // 回调
export const Ref = 0b0000000000010000000; // Ref
export const Snapshot = 0b0000000000100000000; // getSnapshotBeforeUpdate
export const Passive = 0b0000000001000000000; // useEffect
// 组合标记
export const MutationMask = Placement | Update | ChildDeletion | ...;
export const LayoutMask = Update | Callback | Ref;
export const PassiveMask = Passive | ChildDeletion;3. 双缓冲 (Double Buffering) 🔥
3.1 Current 树与 WorkInProgress 树
┌─────────────────────┐ ┌─────────────────────┐
│ Current 树 │ │ WorkInProgress 树 │
│ (当前屏幕显示) │ │ (后台构建中) │
├─────────────────────┤ ├─────────────────────┤
│ FiberRoot │◄────────►│ FiberRoot │
│ │ │ alternate│ │ │
│ ┌────▼────┐ │ │ ┌────▼────┐ │
│ │ App │◄─────┼──────────┼────│ App │ │
│ └────┬────┘ │ │ └────┬────┘ │
│ │ │ │ │ │
│ ┌────▼────┐ │ │ ┌────▼────┐ │
│ │ Child │◄─────┼──────────┼────│ Child │ │
│ └─────────┘ │ │ └─────────┘ │
└─────────────────────┘ └─────────────────────┘3.2 工作流程
javascript
// 1. 首次渲染
// current = null, 创建 workInProgress 树
// 2. 首次渲染完成
// workInProgress 树变成 current 树
// 3. 更新时
// 基于 current 创建 workInProgress (通过 alternate 复用)
// workInProgress.alternate = current
// current.alternate = workInProgress
// 4. 更新完成
// workInProgress 替换 current
// root.current = finishedWork3.3 为什么需要双缓冲?
- 增量渲染: 可以在后台构建新树,不阻塞当前显示
- 快速切换: 构建完成后直接切换指针
- 中断恢复: 中断时 current 树仍完整,可继续渲染
4. Render 阶段 (Reconciliation)
4.1 工作循环 (Work Loop)
javascript
function workLoopConcurrent() {
// 有剩余时间且有工作
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
function workLoopSync() {
// 同步模式不检查时间
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}4.2 performUnitOfWork
javascript
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate;
// 1. beginWork: 向下递归创建子 Fiber
let next = beginWork(current, unitOfWork, renderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 2. 没有子节点,completeWork 向上归
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}4.3 beginWork (递)
深度优先遍历 - "递" 阶段
App (beginWork)
│
▼
Header (beginWork) → Content (beginWork)
│ │
▼ ▼
Logo Title → List
│ │ │
▼ ▼ ▼
null null Items...javascript
function beginWork(current, workInProgress, renderLanes) {
// 根据 Fiber.tag 执行不同逻辑
switch (workInProgress.tag) {
case FunctionComponent:
return updateFunctionComponent(current, workInProgress, ...);
case ClassComponent:
return updateClassComponent(current, workInProgress, ...);
case HostComponent:
return updateHostComponent(current, workInProgress);
// ...
}
}4.4 completeWork (归)
深度优先遍历 - "归" 阶段
null → Logo (complete) → Header (complete)
↓
App (complete)
↑
null → Title → List → Content (complete)javascript
function completeWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case HostComponent: {
if (current !== null) {
// 更新: 对比 props, 打 Update 标记
updateHostComponent(current, workInProgress, ...);
} else {
// 首次渲染: 创建 DOM 节点
const instance = createInstance(type, newProps, ...);
appendAllChildren(instance, workInProgress, ...);
workInProgress.stateNode = instance;
}
// 冒泡 flags
bubbleProperties(workInProgress);
return null;
}
// ...
}
}5. Commit 阶段
5.1 三个子阶段
Commit Phase (同步不可中断)
│
├─── BeforeMutation ─── getSnapshotBeforeUpdate
│
├─── Mutation ───────── 真实 DOM 操作 (增删改)
│ - commitPlacement
│ - commitUpdate
│ - commitDeletion
│
└─── Layout ─────────── componentDidMount/Update
useLayoutEffect5.2 Effect List (React 17-)
在 React 17 之前,使用 Effect List 链表收集有副作用的节点:
firstEffect → Fiber1 → Fiber2 → Fiber3 → lastEffect
│ │ │
Placement Update Deletion5.3 SubtreeFlags (React 18+)
javascript
// React 18 使用 subtreeFlags 标记子树是否有副作用
function bubbleProperties(completedWork) {
let subtreeFlags = NoFlags;
let child = completedWork.child;
while (child !== null) {
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
child = child.sibling;
}
completedWork.subtreeFlags |= subtreeFlags;
}6. Diff 算法 🔥
6.1 三个限制策略
┌─────────────────────────────────────────────────────────────────────┐
│ React Diff 策略 │
├─────────────────────────────────────────────────────────────────────┤
│ 1. 同层级比较: 只对比同一层级,不跨层级移动 │
│ │
│ 2. 类型检测: 类型不同直接销毁重建 │
│ <div> → <span> ❌ 销毁 div 及其子树,创建 span │
│ │
│ 3. Key 优化: 相同 key 认为是同一节点,可复用 │
└─────────────────────────────────────────────────────────────────────┘6.2 单节点 Diff
javascript
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// key 相同
if (child.key === key) {
// type 相同 → 复用
if (child.elementType === element.type) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.return = returnFiber;
return existing;
}
// type 不同 → 删除所有旧节点
deleteRemainingChildren(returnFiber, child);
break;
} else {
// key 不同 → 删除该节点
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 创建新节点
const created = createFiberFromElement(element);
created.return = returnFiber;
return created;
}6.3 多节点 Diff (reconcileChildrenArray)
React 多节点 Diff 分两轮遍历:
第一轮: 处理更新的节点
javascript
// 同时遍历 newChildren 和 oldFiber
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// key 不同,跳出第一轮
if (oldFiber.key !== newChildren[newIdx].key) {
break;
}
// key 相同,尝试复用
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
if (newFiber === null) break; // type 不同
// 复用成功
oldFiber = oldFiber.sibling;
}第二轮: 处理剩余节点
javascript
// 情况1: newChildren 遍历完,删除剩余 oldFiber
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 情况2: oldFiber 遍历完,插入剩余 newChildren
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);
// 插入...
}
return resultingFirstChild;
}
// 情况3: 都有剩余,建立 Map 查找可复用节点
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
if (newFiber !== null) {
// 判断是否需要移动
if (newFiber.alternate !== null) {
existingChildren.delete(newFiber.key ?? newIdx);
}
placeChild(newFiber, lastPlacedIndex, newIdx);
}
}6.4 移动判断 (仅右移策略)
javascript
function placeChild(newFiber, lastPlacedIndex, newIndex) {
newFiber.index = newIndex;
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// 移动: 标记 Placement
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
// 不移动
return oldIndex;
}
} else {
// 新节点: 需要插入
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}示例
旧: A(0) B(1) C(2) D(3)
新: A C D B
lastPlacedIndex = 0
A: oldIndex=0 >= lastPlacedIndex(0), 不移动, lastPlacedIndex = 0
C: oldIndex=2 >= lastPlacedIndex(0), 不移动, lastPlacedIndex = 2
D: oldIndex=3 >= lastPlacedIndex(2), 不移动, lastPlacedIndex = 3
B: oldIndex=1 < lastPlacedIndex(3), 移动!
结果: 只有 B 需要移动到末尾WARNING
这就是为什么将节点从末尾移到开头效率很低 —— 其余所有节点都会被标记为移动。
7. 优先级与 Lanes
7.1 Lane 模型
javascript
export const NoLanes = 0b0000000000000000000000000000000;
export const NoLane = 0b0000000000000000000000000000000;
export const SyncLane = 0b0000000000000000000000000000001; // 同步
export const InputContinuousLane = 0b0000000000000000000000000000100; // 连续输入
export const DefaultLane = 0b0000000000000000000000000010000; // 默认
export const TransitionLane1 = 0b0000000000000000000000001000000; // Transition
// ...
export const IdleLane = 0b0100000000000000000000000000000; // 空闲
export const OffscreenLane = 0b1000000000000000000000000000000; // 离屏7.2 优先级场景
| 优先级 | 场景 | Lane |
|---|---|---|
| 最高 | flushSync, 离散事件 (click) | SyncLane |
| 较高 | 连续输入 (scroll, drag) | InputContinuousLane |
| 默认 | 普通更新 | DefaultLane |
| 较低 | startTransition | TransitionLane |
| 最低 | requestIdleCallback | IdleLane |
8. 面试高频问题
Q1: Fiber 是什么?
Fiber 有三层含义:
- 架构: React 16 的新 Reconciler
- 数据结构: 虚拟 DOM 增强版,链表结构
- 工作单元: 一个 Fiber 对应一次增量任务
Q2: 为什么用链表不用树?
- 可中断: 链表可以记录遍历位置,随时恢复
- 扁平化: 树的递归变成循环迭代
- 三指针: child/sibling/return 完整描述关系
Q3: Render 阶段和 Commit 阶段的区别?
| 阶段 | Render | Commit |
|---|---|---|
| 可中断 | ✅ 可 | ❌ 不可 |
| 副作用 | 无 (纯计算) | 有 (DOM 操作) |
| 输出 | Fiber 树 + Effect 标记 | 真实 DOM |
Q4: React Diff 的时间复杂度?
O(n) — 通过三条策略将 O(n³) 优化为 O(n)
Q5: 为什么 key 用 index 不好?
javascript
// 错误示例: 删除第一个元素
items = ['A', 'B', 'C'] → ['B', 'C']
// key=[0,1,2] → key=[0,1]
// React 认为: 0→0复用, 1→1复用, 删除2
// 实际效果: A变B, B变C, 删除C (错误!)Q6: 双缓冲的作用?
- 后台构建新树,不阻塞当前显示
- 构建完成快速切换
- 中断时 current 树保持完整
Q7: React 18 的 Concurrent 特性基于什么实现?
基于 Fiber 架构的可中断渲染 + Lane 优先级模型 + Scheduler 调度器