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 15 | React 16+ |
|---|---|---|
| 架构 | Stack | Fiber |
| 递归 | 深度递归,无法中断 | 链表遍历,可中断 |
| 主线程 | 易阻塞 | 分片执行 |
| 优先级 | 无 | 5 级优先级 |
| 双缓冲 | 无 | 有 current + WIP |
| Hooks | 不支持 | 完全支持 |
| 并发渲染 | 不支持 | 支持(Concurrent Mode) |
| 性能 | 大树卡顿 | 大树流畅 |
核心理念:用链表代替递归,用时间分片代替一次性完成。