React Fiber 架构
Fiber 是 React 16 引入的新协调(Reconciliation)架构,本质上是「可中断、可恢复、可优先级调度」的渲染机制。它把旧版「一口气递归到底」的更新,改成「能做多少做多少,时间不够就停」。
一、为什么需要 Fiber
React 16 之前用的是 Stack Reconciler,更新流程是同步递归:
setState
-> 递归 diff 整棵树
-> 一次性同步执行到底
-> commit DOM问题在于:调用栈递归一旦开始就无法暂停。当页面有 5000 个组件需要更新时,React 必须一次性算完。而 JS 是单线程,渲染期间用户的点击、输入、动画、滚动全部无法响应——这就是主线程阻塞。
Fiber 的核心思想:把「递归不可中断任务」改成「可拆分的小任务」。
旧:一口气干完
新:干一点 → 暂停 → 浏览器先响应用户 → 有空再继续二、Fiber 的数据结构
Fiber 不再用递归调用栈,而是用链表节点记录遍历进度。每个组件对应一个 FiberNode:
interface FiberNode {
type: any // 组件类型
stateNode: any // 对应的真实 DOM 或组件实例
child: FiberNode | null // 第一个子节点
sibling: FiberNode | null // 下一个兄弟节点
return: FiberNode | null // 父节点
alternate: FiberNode | null // 指向双缓存中的另一棵树
}| 字段 | 作用 |
|---|---|
child | 第一个子节点 |
sibling | 下一个兄弟节点 |
return | 父节点 |
alternate | 双缓存树的另一棵 |
为什么用链表而不用递归? 递归无法暂停,而链表遍历可以让 React 自己控制「下一个执行谁」「什么时候暂停」,这是 Fiber 能中断的核心。
三、逐行拆解 workLoop
workLoop 是 Fiber「可中断循环」的最小骨架(教学版,类似 Dan Abramov 的 Didact):
function workLoop(deadline) {
while (nextUnit && deadline.timeRemaining() > 0) {
nextUnit = performUnitOfWork(nextUnit)
}
if (nextUnit) requestIdleCallback(workLoop)
else commitRoot()
}循环条件:nextUnit(还有没处理完的节点)和 deadline.timeRemaining() > 0(本帧还有空闲时间)必须同时满足,任一不满足就跳出。这就是「时间切片」的开关——靠时间判断决定要不要让出主线程。
进度保存:nextUnit 是模块级变量而非局部变量,它记录了遍历进度。即使循环被打断退出,进度也保存在它身上,下次进来能接着做——这是断点恢复的关键。
两种退出的区分:
| 退出原因 | nextUnit 状态 | 后续动作 |
|---|---|---|
| 时间用光(被打断) | 还有值 | requestIdleCallback 重新排队,下一帧继续 |
| 任务全部完成 | 为空 | 进入 commitRoot,更新 DOM |
真实 React 并未直接用
requestIdleCallback(触发频率不稳、兼容性差),而是用自己的 Scheduler 包,底层基于MessageChannel模拟出「空闲回调 + 优先级」的能力。原理一致。
四、两个阶段:Render 与 Commit
Fiber 把更新分成两个阶段:
Render 阶段(协调阶段):构建新 Fiber 树、diff 新旧节点、收集副作用。可以暂停、恢复、终止。
// 核心循环:一次只处理一个 Fiber 节点
while (workInProgress && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress)
}shouldYield() 判断「是否该把控制权还给浏览器」,时间快到时返回 true,React 就暂停。
Commit 阶段:真正修改 DOM(appendChild、removeChild、更新属性、执行 useEffect)。不可中断,因为 DOM 必须保证一致性,否则页面会出现「更新一半」的状态。
五、时间切片(Time Slicing)
时间切片就是把长任务切成很多小片段执行:
旧:一个任务执行 200ms → 用户感觉卡顿
新:5ms React → 5ms 浏览器
→ 5ms React → 5ms 浏览器 → 页面不卡,动画流畅为什么留时间给浏览器? 浏览器一帧约 16.6ms(60fps),React 每做一小段就让出主线程,保证浏览器有时间渲染动画、响应输入。
六、双缓存树(Double Buffering)
Fiber 同时维护两棵树,通过 alternate 字段互相指向:
current tree (当前显示的树,不动)
workInProgress tree (正在构建的新树)更新时旧树不动,在内存中构建新树;全部完成后直接切换指针:
// 一次性切换,避免用户看到中间状态
root.current = workInProgress这样保证用户要么看到旧画面、要么看到新画面,不会看到构建到一半的树。
七、Fiber 解决了什么
| 问题 | 旧 React | Fiber |
|---|---|---|
| 页面卡顿 | 长时间阻塞主线程 | 可中断渲染 |
| 动画掉帧 | 无法插队 | 优先处理高优先级任务(点击、输入) |
| 并发特性 | 不支持 | 支撑 Concurrent、Suspense、transition、selective hydration |
React 18 的并发能力(Concurrent Rendering、Suspense、useTransition)全部依赖 Fiber 架构。
核心三句话:Fiber 把每个组件转成 FiberNode,用链表遍历替代递归,从而能暂停和恢复。Render 阶段可中断、负责计算和 diff;Commit 阶段不可中断、负责真实 DOM 更新。时间切片让 React 每帧只占用一小段时间,把主线程及时还给浏览器,避免卡顿。