Fiber 架构精练总结 | 易记版

4 阅读7分钟

Fiber 架构精练总结 | 易记版

用最简洁的语言理解 Fiber 的本质和原理


核心概念 3 句话速记

Fiber = 可中断的工作单位

React 16+ 用 Fiber 代替递归
使用时间分片,不阻塞主线程

为什么需要 Fiber?

React 15 问题:递归无法中断

React 15 (Stack Reconciler)
  ↓
  递归式遍历组件树
  ↓
  一旦开始,无法暂停
  ↓
  若树深度大 → 主线程被占用数秒
  ↓
  用户输入无响应、动画卡顿 ❌

React 16+ 方案:链表可中断

React 16+ (Fiber Reconciler)
  ↓
  用链表替代递归
  ↓
  可暂停、恢复、终止
  ↓
  分时间片执行(5ms 工作 + 5ms 让出主线程)
  ↓
  用户交互、动画流畅 ✅

Fiber 数据结构

最小化的 Fiber 节点

// Fiber 就是一个对象,存储组件信息
interface Fiber {
  // 1. 类型信息
  type: string | Function;        // 'div' | MyComponent
  key: string | null;             // React key
  
  // 2. 树结构关系
  parent: Fiber | null;           // 父 Fiber
  child: Fiber | null;            // 第一个子 Fiber
  sibling: Fiber | null;          // 下一个兄弟 Fiber
  
  // 3. 双缓冲指针
  alternate: Fiber | null;        // 对应的旧 Fiber
  
  // 4. 状态数据
  props: object;                  // 传入的属性
  state: any;                     // 组件的 state
  hooks: Hook[];                  // useXxx 的 hooks 链表
  
  // 5. 工作标记
  effectTag: 'PLACEMENT' | 'UPDATE' | 'DELETION' | null;
  effects: Fiber[];               // 本节点的 effect
  nextEffect: Fiber | null;       // 链接下一个有 effect 的节点
  
  // 6. 输出
  stateNode: any;                 // 对应的 DOM 节点
}

3 种关键指针关系

1. 树结构指针:parent ← → child ← → sibling
   ┌─ A (parent)
   ├─ B (child 1)
   ├─ C (sibling of B)
   └─ D (sibling of C)

2. 双缓冲指针:current ↔ alternate
   current (旧树) ↔ alternate (新树)
   渲染完成后互相替换

3. Effect 链表:effectTag 标记 + nextEffect 链接
   有更新的节点链接成单链表
   便于后续统一处理

两棵树 | 双缓冲机制

理解双缓冲

屏幕显示     内存构造
  ↓            ↓
current 树  ← → workInProgress 树
(旧树)           (新树)

工作流程:
1. 完全在内存中构造 WIP 树(不影响屏幕显示)
2. Diff 计算,标记变更(PLACEMENT/UPDATE/DELETE)
3. 构造完成后 → commit
4. 执行 DOM 变更
5. current = WIP (原子性切换)
6. WIP = current (为下一次做准备)

为什么用双缓冲?

✅ 保证 DOM 一致性:不会出现中间状态
✅ 高效复用:旧树的节点可复用
✅ 减少 GC 压力:Fiber 对象可复用
❌ 避免:频繁创建销毁新 Fiber

工作流程 | 分两个阶段

阶段 1:Render(可中断)

输入:新的 props 和 state
↓
遍历 Fiber 树 → Diff 算法
↓
标记需要更新的节点
↓
输出:effectTag 列表
↓
特点:可暂停、可恢复

具体步骤

while (workInProgress) {
  // 处理当前 Fiber
  performUnitOfWork(workInProgress);
  
  // 如果主线程需要处理其他事务(如用户输入)
  if (shouldYield()) {
    break;  // 暂停!稍后继续
  }
  
  // 获取下一个要处理的 Fiber
  workInProgress = getNextFiber();
}

阶段 2:Commit(不可中断)

输入:Render 阶段的 effectTag 列表
↓
执行 DOM 操作
↓
执行 useLayoutEffect(同步)
↓
提交完毕
↓
调度 useEffect 的异步回调
↓
特点:原子性,一次性

为什么不能中断?

✗ 中断会导致 DOM 不一致
✗ 用户看到中间状态
✗ useLayoutEffect 无法同步执行

虽然快速,一般只需 5-10ms,不是问题

调度系统 | Scheduler

时间分片的执行模式

┌─ 每帧 16.67ms (60fps)
│
├─ 0-5ms:JavaScript 执行(Fiber 工作)
├─ 5-10ms:检查是否有高优先级任务
├─ 10-16ms:浏览器渲染(Layout + Paint + Composite)
│
└─ 若任务未完成 → 进入下一帧继续

时间分片伪代码

function workLoopConcurrent() {
  while (workInProgress && !shouldYield()) {
    performUnitOfWork(workInProgress);
    workInProgress = getNextFiber();
  }
}

function shouldYield() {
  // 检查当前帧是否还有时间
  const timeRemaining = deadline - now();
  return timeRemaining <= 1; // 留 1ms 余量
}

// 浏览器提供 API:scheduler.scheduleCallback
scheduler.scheduleCallback(
  ImmediatePriority,
  workLoopConcurrent
);

优先级系统 | 5 个等级

优先级对应的任务类型

1. ImmediatePriority (立即)
   ├─ 受控表单输入(onClick)
   └─ 用户输入

2. UserBlockingPriority (阻塞用户)
   ├─ 动画
   ├─ 悬停事件
   └─ 一般事件

3. NormalPriority (常规)
   ├─ setState 更新
   ├─ 网络请求
   └─ 分析事件

4. LowPriority (低)
   └─ Suspense 预加载

5. IdlePriority (空闲)
   └─ 离屏更新

优先级如何运作?

高优先级任务:立即插队中断低优先级任务

示例:
1. 用户输入高优先级任务
2. setState 正在执行 (NormalPriority)
3. Render 阶段被中断 ← 重启 render
4. 重新计算组件树(因为优先级更新了)
5. 完成后再继续低优先级

结果:输入响应快 ✅

Diff 算法 | 怎样比较变化

三条启发式规则

规则 1:不同类型的元素产生不同的树
  <div></div><span></span>
  结果:删除 div 树,创建 span 树

规则 2:key 属性提示哪些元素是稳定的
  <Item key="1" /><Item key="1" />
  结果:复用节点,只更新 props

规则 3:开发者可以提示哪些子树保持不变
  useMemo(() => <SubTree />, deps)

列表渲染的 key 很重要

// ❌ 错误:使用 index 作为 key
{items.map((item, index) => (
  <div key={index}>{item}</div>  // 重新排序时混乱!
))}

// ✅ 正确:使用唯一标识
{items.map(item => (
  <div key={item.id}>{item}</div>  // 稳定复用
))}

几个重要概念

1. 什么时候开始 Render?

触发情况:
├─ setState 调用
├─ props 改变
├─ context 改变
├─ forceUpdate
└─ Hooks 更新

React 的做法:
1. 创建新的 WIP 树
2. 启动 Render 工作循环
3. 调度器在适当时间执行

2. Render 和 Commit 的区别

Render 阶段
├─ 可中断
├─ 可重新执行
├─ 不产生副作用
├─ 可能执行多次
└─ 用于计算变更

Commit 阶段
├─ 不可中断
├─ 一次性执行
├─ 执行 DOM 变更、useLayoutEffect
├─ 产生副作用
└─ 用于实现变更

3. Hooks 的存储

每个 Fiber 对应一条 Hook 链表

Fiber.hooks = [  { state: count, queue: [...] },     // useState
  { deps: [...], fn, cleanup },       // useEffect
  { current: ref },                   // useRef
  ...
]

调用顺序决定身份:
const [a] = useState(0);  // 第 1 个 Hook
const [b] = useState(1);  // 第 2 个 Hook

// ❌ 在条件语句中调用 → 顺序变化 → 混乱!
if (condition) {
  const [c] = useState(2);  // 变成第 1 个了!
}

性能优化指南

三个优化方向

1. 减少 Render 阶段的工作量
   ├─ useMemo:缓存计算
   ├─ useCallback:缓存函数
   ├─ React.memo:缓存组件
   └─ 合理分割状态

2. 减少 Commit 阶段的 DOM 操作
   ├─ 避免频繁 DOM 查询
   ├─ 批量更新
   └─ 避免强制同步布局

3. 减少需要处理的节点数
   ├─ 虚拟列表(react-window)
   ├─ 延迟加载(Suspense)
   └─ 代码分割

常见优化代码

// 1. 缓存计算结果
const memoValue = useMemo(() => {
  return expensiveCalculation(props.value);
}, [props.value]);

// 2. 缓存回调
const memoCallback = useCallback(() => {
  handleClick();
}, [deps]);

// 3. 缓存组件
const MemoComponent = React.memo(Component, (prev, next) => {
  return prev.value === next.value;  // true 不重新渲染
});

// 4. 分割大组件
const SubComponent1 = React.memo(...)
const SubComponent2 = React.memo(...)
// 各自独立渲染,不互相影响

面试速记要点

必须记住的 5 点

1️⃣  Fiber 的本质
    = 一个 JavaScript 对象
    = 存储组件的结构、状态、效果
    = 可中断的最小工作单位

2️⃣  为什么需要 Fiber
    React 15 递归无法中断 → 主线程阻塞
    React 16+ 用链表支持中断 → 时间分片

3️⃣  两个阶段
    Render(可中断,计算变更)
    Commit(不可中断,实施变更)

4️⃣  双缓冲机制
    同时维护两棵树:current 和 workInProgress
    保证原子性和高效复用

5️⃣  优先级系统
    不同类型任务有不同优先级
    高优先级任务可中断低优先级任务

高频面试题答案框架

Q: Fiber 是什么?
A: "Fiber 是 React 16+ 的核心架构,
   本质是一个对象,存储组件信息。
   它将递归更新改为可中断的链表遍历,
   使用时间分片,防止主线程长时间阻塞。"

Q: Render 和 Commit 有什么区别?
A: "Render 可中断,用于计算变更;
   Commit 不可中断,用于实施变更。
   这样设计是为了在 Render 时可以让出主线程,
   但 Commit 时必须一次性完成保证 DOM 一致性。"

Q: 为什么 useHook 不能在条件语句中使用?
A: "因为 Fiber 用数组索引存储 hooks,
   调用顺序决定了 hook 的身份。
   条件语句改变顺序,会导致 hook 混乱。"

Q: 双缓冲机制的优势是什么?
A: "可以在不影响屏幕显示的情况下,
   在内存中构造新的 Fiber 树。
   完成后一次性替换,保证原子性,
   旧树的节点还可复用。"

总结对比表

对比项React 15React 16+
架构StackFiber
递归深度递归,无法中断链表遍历,可中断
主线程易阻塞分片执行
优先级5 级优先级
双缓冲有 current + WIP
Hooks不支持完全支持
并发渲染不支持支持(Concurrent Mode)
性能大树卡顿大树流畅

核心理念:用链表代替递归,用时间分片代替一次性完成。