React 源码面试冲刺 - Day 4
日期:2026-03-19 主题:Fiber 架构
📖 Day 4 核心内容:Fiber 架构
👴 老大爷能听懂版
Fiber = React 的"重新设计的大脑"
| 旧架构(Stack Reconciler) | 新架构(Fiber) |
|---|---|
| 递归更新,无法中断 | 可中断、可恢复 |
| 一旦开始必须完成 | 可以"先歇歇"再继续 |
| 主线程被阻塞 | 空闲时间接着干 |
| 同步进行 | 异步进行 |
Fiber 的核心思想:把工作拆成小块!
想象一下:
- 旧版本:你要搬家,一口气把所有东西搬完,累死
- Fiber 版本:每次搬一点,搬累了休息一下喝口水���然后再搬
Fiber 节点是什么?
- 每个 React 元素对应一个 Fiber 节点
- Fiber 节点是链表结构,不是树!
- 可以无限延伸(因为是链表)
<div> FiberNode
<h1>Title</h1> → child → child → child
<p>Text</p> → child
</div>
Fiber 的三要素:
- 是什么: 一个链表节点,代表一个工作单元
- 从哪里来: React Element 转换而来
- 到哪里去: 最终变成 DOM 节点
💻 专业开发者版
Fiber 节点结构:
type Fiber = {
// 身份标识
tag: WorkTag, // 组件类型:FunctionComponent、ClassComponent...
key: null | string, // 识别符
type: any, // 组件定义(函数/类/字符串)
// 链表结构
return: Fiber | null, // 父节点
child: Fiber | null, // 第一个子节点
sibling: Fiber | null, // 下一个兄弟节点
index: number, // 在兄弟中的位置
// 状态
pendingProps: any, // 新的 props
memoizedProps: any, // 渲染用的 props
memoizedState: any, // 组件状态(hooks 链表)
// 更新相关
updateQueue: mixed, // 待处理的更新队列
alternate: Fiber | null, // 旧 Fiber(用于对比)
// 副作用
flags: Flags, // 需要执行的副作用
subTreeFlags: Flags, // 子树的副作用
deletions: Fiber[], // 需要删除的节点
// 调试
_debugID: number,
_debugSource: any,
}
Fiber 的工作流程:
Render 阶段(可中断)
↓
1. beginWork() → 从根节点向下遍历
2. diffFiber() → 对比新旧 Fiber
3. completeWork() → 完成当前分支
Commit 阶段(不可中断)
↓
1. BeforeMutation → DOM 更新前
2. Mutation → 实际 DOM 操作
3. Layout → 布局后 effect
4. Passive → useEffect 触发
Fiber 的核心优势:
| 优势 | 说明 |
|---|---|
| 可中断 | 优先级高的任务可以插队 |
| 可恢复 | 中断后能接着干,不丢失进度 |
| 优先级调度 | 紧急任务(如输入)优先处理 |
| 并发支持 | 同时处理多个 Fiber 树 |
React 的优先级机制:
// 优先级从高到低
export const PriorityLevel = {
ImmediatePriority: 1, // 最高:点击、输入
UserBlockingPriority: 2, // 用户Blocking:滚动
NormalPriority: 3, // 普通:数据加载
LowPriority: 4, // 低:内容刷新
IdlePriority: 5, // 最低:预加载
};
key 和 alternate:
alternate:双缓冲技术,旧 Fiber 保留,上一份和这一份对比- 这样可以实现"无缝"更新,不会出现页面闪烁
💪 面试高频问题
| 问题 | 答案要点 |
|---|---|
| 什么是 Fiber? | React 16 引入的新协调引擎,链表结构的工作单元 |
| Fiber 解决了什么问题? | 主线程阻塞、无法优先级调度、不能中断恢复 |
| Fiber 和 Virtual DOM 的关系? | Fiber 是 Virtual DOM 的升级版,从树变链表 |
| React 如何保证页面不卡顿? | 任务拆分 + 时间片 + requestIdleCallback |
| Fiber 的 Render 和 Commit 区别? | Render 可中断、Commit 必须同步完成 |
| Fiber 节点的数据结构? | child/sibling/return 链表,alternate 双缓冲 |
📊 Day 4 面试能力评估
Day 1-4 学完能答:
- ✅ HTML 基础概念
- ✅ React 核心 API
- ✅ JSX 转换原理
- ✅ Hooks 原理
- ✅ Fiber 架构 ← 今日新增
还需要 Day 5+:
- ❌ Diff 算法
- ❌ 调度器原理
- ❌ 并发模式
- ❌ 状态管理原理
💪 今日自测
- 为什么 Fiber 用链表而不是树?
- Fiber 的 Render 阶段和 Commit 阶段有什么区别?
- React 怎么做到���输入不卡顿的?(优先级机制)
- alternate 在 Fiber 中起什么作用?
📝 详细答案
1. 为什么 Fiber 用链表而不是树?
核心原因:链表支持中断和恢复,树上不行
| 对比项 | 树结构 | 链表结构 |
|---|---|---|
| 遍历方式 | 递归(无法中断) | 循环(可以暂停) |
| 内存占用 | 固定、连续 | 分散、不连续 |
| 中断恢复 | 困难(不知道停在哪) | 简单(保存指针就行) |
| 父子关系 | 严格树形 | 可灵活指向 |
具体解释:
旧架构:树 + 递归
function walk(node) {
process(node); // 处理当前
node.children.forEach(walk); // 递归子节点
// ❌ 一旦开始根本无法停止,必须一口气跑完
}
新架构:链表 + 循环
function workLoop() {
while (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// ✅ 每次处理一个 Fiber 节点,处理完可以暂停
if (shouldYield()) {
return; // 让出主线程,下次继续
}
}
}
链表结构的优势:
child:第一个子节点sibling:下一个兄弟节点return:父节点
这样可以从任意节点"断掉",下次继续从"断掉"的地方开始。
2. Fiber 的 Render 阶段和 Commit 阶段有什么区别?
| 阶段 | Render | Commit |
|---|---|---|
| 可中断? | ✅ 可以 | ❌ 必须同步完成 |
| 执行方式 | 异步 | 同步 |
| 主要工作 | 计算差异,收集副作用 | 实际操作 DOM |
| 耗时 | 可能很长(可拆分) | 必须尽快完成 |
Render 阶段(可中断)���
// 伪代码
function renderRoot(root) {
workInProgress = root;
while (workInProgress) {
// 处理一个 Fiber 节点
workInProgress = performUnitOfWork(workInProgress);
// 检查是否要让出主线程
if (shouldYield()) {
// 👻 中断!保存状态,下次继续
return;
}
}
// 全部处理完,进入 Commit
commitRoot();
}
Render 阶段内部流程:
beginWork()
↓
根据 Fiber 类型的处理
↓
reconcileChildren() → 对比 children
↓
completeWork()
Commit 阶段(不可中断):
function commitRoot(root) {
// 1️⃣ DOM 更新前
commitBeforeMutationEffects();
// 2️⃣ 实际 DOM 操作(可能会阻塞)
commitMutationEffects();
// 3️⃣ 布局后 effects(同步)
commitLayoutEffects();
// 4️⃣ useEffect(异步)
commitPassiveEffects();
}
为什么 Commit 不能中断?
- 因为用户能看到页面变化了,必须保证一致性
- 如果中断了,页面可能显示一半,用户体验极差
3. React 怎么做到让输入不卡顿的?(优先级机制)
核心:高优先级任务可以打断低优先级任务
// React 的优先级定义(简化版)
const priorities = {
ImmediatePriority: 1, // 🔴 最急:用户输入、点击
UserBlockingPriority: 2, // 🟠 较急:滚动
NormalPriority: 3, // 🟡 普通:网络请求、渲染
LowPriority: 4, // 🟢 不急:后台刷新
IdlePriority: 5, // ⚪ 最闲:预加载
};
工作流程:
用户输入 → 触发更新
↓
React 分配 ImmediatePriority(最高)
↓
中断当前低优先级任务
↓
立即处理用户输入
↓
处理完继续之前的低优先级任务
实际例子:
场景:正在渲染一个大列表,用户突然点击按钮
1. 本来在渲染列表(NormalPriority)
2. 用户点击按钮 → 触发更新(ImmediatePriority)
3. React 检测到更高优先级,中断渲染
4. 立即处理按钮点击 → 更新 UI
5. 处理完再回头继续渲染列表
怎么实现的?
// 每次处理完一个 Fiber,检查是否要让行
function workLoop() {
while (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// 关键:如果当前任务的优先级 < 新任务的优先级
// 让出主线程!
if (currentPriority < nextPriority) {
scheduleCallback(flushWork); // 下次继续
return;
}
}
}
总结:Fiber + 优先级 = 输入不卡顿
4. alternate 在 Fiber 中起什么作用?
alternate = 双缓冲,实现"无缝"更新
// 每个 Fiber 都有 alternate 指向旧版本
fiber.alternate = oldFiber; // 旧 Fiber
双缓冲技术:
当前 Fiber 树(workInProgress) 旧 Fiber 树(current)
┌─────────────────┐ ┌─────────────────┐
│ root │ │ root │
│ child: A │ ─── alternate ───→ child: A │
│ child: B │ │ child: B │
│ child: C │ │ child: C │
└─────────────────┘ └─────────────────┘
为什么需要 alternate?
- 对比差异:新 Fiber 和旧 Fiber 一一对比,找出哪里要更新
- 复用节点:如果对比发现没变化,直接复用旧节点(节省内存)
- 快照备份:如果更新中途失败,可以回滚到旧状态
复用示例:
// 对比新旧 Fiber
function reconcileChildren(current, newChildren) {
return newChildren.map((child, index) => {
const oldChild = current?.[index];
// 关键:判断是否可以复用
if (canReuseScheduleUnitOfWork(oldChild, child)) {
// ✅ 复用!只要标记_update
return {
...oldChild,
pendingProps: child.props, // 更新属性
};
}
// ❌ 不能复用,创建新的
return createFiber(child);
});
}
实际效果:
场景:只改了按钮文字
旧 Fiber 树:
<button>OK</button> → 保存
新 Fiber 树:
<button>Submit</button> → 只标记 textUpdate
结果:
- 其他没改的节点完全复用,不重新创建
- ⚡ 性能提升显著!
完整流程:
更新触发
↓
创建 workInProgress 树(拷贝 current)
↓
遍历同时对比 current 和 workInProgress
↓
标记需要更新的节点(flags)
↓
Commit 阶段只更新标记的节点
↓
完成!workInProgress 变成新的 current
恭喜 Day 4 学完!Fiber 核心在于:链表可中断 + 优先级可插队 + 双缓冲复用 🎉