🎯 React Fiber 的出现
React16 之前的渲染模式是不可中断的,如果目前执行的任务过长,就会出现以下问题:
- 用户点按钮半天没反应
- 动画卡得像 PPT
- 页面像被人按下了暂停键
为了解决 React 渲染不友好的问题,让 React 变得更灵活(被打断也可以继续),所以需要一个全新的运行时架构
- 可切片
- 可暂停
- 可恢复
- 可优先级调度
🧠 React Fiber 的定义
Fiber 是可中断的渲染引擎,是把组件更新变成“可暂停的工作单元”的调度系统。
更具体的理解就是:
- Fiber 是一种链表结构的 vnode
- 它把每一个组件渲染拆成任务单元(unit of work)
- 所有任务都会被调度器按照优先级先后处理
🧩 React Fiber 的关键能力
💪 1. 可中断渲染(Time Slicing)
| 之前 | 现在 |
|---|---|
| 渲染不可中断,会阻塞其他事件,如用户点击等 | 渲染可中断,在任务执行过程中随时会给浏览器让步,等高优任务结束后再恢复执行 |
🎚️ 2. 优先级调度(Scheduler)
React Fiber 会给每个更新都贴上“优先级标签”,像这样:
| 更新类型 | 优先级(lane) |
|---|---|
| 用户点击 | 🟥 超高优先级 |
| 输入框输入 | 🟧 很高 |
| 普通渲染 | 🟩 中等 |
| transition | 🟦 低一点 |
| 日志、统计 | 🟪 淡淡的随缘优先级 |
用户点击了按钮 => 触发最高lane 任务 异步数据回来 => 触发中等lane任务 transition动效更新 => 触发更低lane任务
React 会根据这些任务的 lane 来决定当前应该执行哪个任务,以及后续任务的执行顺序。
🐱 3. 可恢复渲染(Pause & Resume)
React Fiber 中的每个节点都是一个 Fiber 对象,每次渲染一个 Fiber,都会在调度器中记录一个TODO List
Fiber A:处理中...
Fiber B:处理中...
Fiber C:处理中...
--给浏览器让步--
Fiber D:继续处理...
🔁 4. 双缓冲结构(alternate)
每个 Fiber 对象都有一个 alternate指针,分别指向两棵树:
current:现在正在屏幕上的版本workInProgress:正在构建的新版本
渲染期间 React 在 workInProgress 上完成所有的 diff 对比及更新操作,等渲染完毕后再将所有更改全部更新到current上面。
🧱 React Fiber 的执行流程
整个更新流程参考图如下所示:
🐱 1. 触发更新(setState / dispatch)
↓
🐱 2. 调度(分配 lane优先级)
↓
🐱 3. render 阶段(可中断)
- beginWork(创建新 fiber)
- completeWork(收集副作用 flags)
↓
🐱 4. commit 阶段(不可中断)
- mutation:动 DOM
- layout:执行 layout effect
- passive:执行普通 effect
render 可以被打断,commit 绝对不会被打断
- Render 阶段 = 列购物清单(只写要买什么,不真买)
- Commit 阶段 = 拿着清单去超市(真花钱、真拿货)
📝 Render 阶段:列清单
React 遍历组件树,找出「哪些地方需要更新」,但不动真实 DOM。
// 你的代码
function App() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}
// Render 阶段做的事:
// 1. 发现 count 从 0 变成 1
// 2. 算出 div 的内容要改成 "1"
// 3. 记下来:{ 操作: '更新文本', 目标: div, 新内容: '1' }
// 4. 继续看下一个组件...
① 可以中断
// 就像写清单时,可以接个电话再回来继续写
while (有任务 && 没超时) {
处理一个节点();
}
if (还有任务) {
下次空闲继续(); // 不阻塞用户操作
}
② 走两遍(DFS)
向下走(beginWork):看这个组件要不要更新
向上走(completeWork):把子组件的结果汇总给父组件
Render 阶段遍历 workInProgress 树,进行 diff 计算,把发现的需要改动的地方记在副作用链表上。
Render 阶段开始
↓
遍历 Fiber 树
↓
发现 A 需要更新 → 把 A 加入链表:A → null
↓
发现 B 需要更新 → 把 B 接在后面:A → B → null
↓
发现 C 需要更新 → 把 C 接在后面:A → B → C → null
↓
遍历结束
↓
【产物】root.firstEffect = A → B → C → null
workInProgress 树(完整结构)
├── div
│ ├── p (不变)
│ └── span (变了) ← 标记 effectTag='UPDATE'
└── footer (不变)
↓
副作用链表(只记变化)
UPDATE → null
↑
这个 span 节点(引用自 workInProgress 树)
↓
Commit 阶段
拿到链表 → 找到 span 节点 → 看 workInProgress 树知道它该变成什么样 → 更新 DOM
🔧 Commit 阶段:去超市
Commit 阶段操作的是 workInProgress 树对应的 DOM,操作完成后交换指针,让 workInProgress 变成新的 current,整个过程就像「拿着新衣服穿上,然后标签改成新衣服」。
function commitRoot(root) {
// 1. 操作 DOM(基于 workInProgress)
commitMutationEffects(root.finishedWork);
// 2. DOM 已经更新完成
// 3. ⚡️ 交换指针(一瞬间)
root.current = root.finishedWork; // workInProgress 变成新的 current
// 4. 现在状态一致了
// current = 新树 (props 和 DOM 都对了)
// workInProgress = 旧树 (下次备用)
// 5. 执行生命周期
commitLayoutEffects(root.current);
}
三大步骤
// 1. before mutation(动手前)
执行 getSnapshotBeforeUpdate // 比如:记录滚动位置
// 2. mutation(动手!)
真正操作 DOM // 插入、删除、更新属性
// 3. layout(动手后)
执行 componentDidMount/Update // 比如:读取 DOM 位置
执行 useEffect
🎭 还是装修的例子
| 场景 | Render 阶段 | Commit 阶段 |
|---|---|---|
| 刷墙 | 量尺寸、算材料、画图纸 | 搬家具、刷漆、收拾 |
| 做饭 | 想菜谱、列食材、备菜 | 开火炒、装盘、上桌 |
| 网购 | 逛淘宝、加购物车 | 下单付款、收货 |
关键:Render 阶段可以「逛累了歇会儿」,Commit 阶段必须「付款一气呵成」!
💡 为什么这么设计?
1. 不卡顿
// 如果没有两个阶段
一次性计算+更新 10000 个节点 → 页面卡死 200ms
// 有了两个阶段
Render: 分 5 次计算,每次 5ms,中间让用户点鼠标 ✅
Commit: 一次性更新,只卡 20ms,用户感觉不到
2. 保证一致性
// Commit 必须一次性
如果更新一半被打断 → 页面显示一半新一半旧 → 错乱 ❌
3. 优先级调度
用户点击按钮(高优先级)→ 打断数据加载(低优先级)→ 先处理点击
| 对比项 | Render 阶段 | Commit 阶段 |
|---|---|---|
| 通俗理解 | 列购物清单 | 去超市买单 |
| 操作对象 | 虚拟 DOM(Fiber 树) | 真实 DOM |
| 能否中断 | ✅ 能 | ❌ 不能 |
| 主要工作 | 找不同、列计划 | 执行计划 |
| 相关生命周期 | render, shouldUpdate | DidMount, DidUpdate |
| 开发体验 | 感觉不到 | 感觉 DOM 变了 |
🎯 双缓冲结构
双缓冲就是 React 在内存里同时维护两棵 Fiber 树,一棵展示当前界面(current),一棵构建下一个界面(workInProgress),就像PPT的「当前页」和「编辑页」。
🌲 两棵树:current 和 workInProgress
// React 内存中同时存在两棵 Fiber 树
const fiberTree = {
current: '正在屏幕上显示的树', // 用户当前看到的界面
workInProgress: '正在后台构建的树' // 下一个要展示的界面
};
| 场景 | current 树 | workInProgress 树 |
|---|---|---|
| PPT 演示 | 大屏幕上正在放的页面 | 你在电脑上偷偷改的下一页 |
| 装修房子 | 你正在住的房子 | 隔壁正在装修的新房 |
| 换衣服 | 身上穿的衣服 | 手里拿着的要换的衣服 |
Render 阶段:构建 workInProgress 树
function renderPhase(root) {
// 1. 拿到当前显示的树
const currentTree = root.current;
// 2. 创建或复用 workInProgress 树
let workInProgress = currentTree.alternate;
if (!workInProgress) {
// 第一次渲染,创建新树
workInProgress = createFiberFrom(currentTree);
currentTree.alternate = workInProgress;
workInProgress.alternate = currentTree;
}
// 3. 在 workInProgress 树上进行 Render 阶段工作
performWork(workInProgress);
// 4. Render 结束,workInProgress 树准备好了
// 但它还在内存里,没显示到屏幕
}
关键:用户在屏幕上看到的还是 current 树,完全不受影响!
🧭 alternate 指针
// 每个 Fiber 节点都有一个 alternate 指向对方的对应节点
const fiberNode = {
stateNode: <div>, // 真实 DOM
child: ..., // 子节点
sibling: ..., // 兄弟节点
// 关键:指向另一棵树上对应的节点
alternate: {
// 这是另一棵树上的同一个组件
stateNode: <div>, // 指向同一个 DOM!
child: ...,
sibling: ...,
alternate: fiberNode // 互相指向
}
};
两棵树共享同一个真实 DOM 节点,只是状态不同!
Commit 阶段:交换指针
function commitRoot(root) {
// 1. 拿到已经构建好的 workInProgress 树
const finishedWork = root.workInProgress;
// 2. 执行所有 DOM 操作(mutation 阶段)
commitMutationEffects(finishedWork);
// 3. ⚡️ 关键步骤:交换指针!
// 让 workInProgress 树变成新的 current 树
root.current = finishedWork;
// 4. 旧的 current 树变成下一次的 workInProgress
// 下次 Render 时可以直接复用
}
指针交换的瞬间
// 交换前
root.current = 旧树 (正在显示)
root.workInProgress = 新树 (构建完成,还没显示)
// 执行 DOM 更新(用户看到界面变了)
// 交换指针(一瞬间完成)
root.current = 新树 (现在显示这个)
root.workInProgress = 旧树 (变成下一次的草稿)
就像:PPT 点击「下一页」的那一瞬间,新页面出现,旧页面变成草稿
💡 为什么需要双缓冲?
- 防止页面闪烁
// 如果没有双缓冲
function update() {
删除旧DOM(); // 用户看到界面没了 ❌
创建新DOM(); // 白屏一下再出现
}
// 有双缓冲
function update() {
在内存构建新树(); // 用户啥也看不见
一次性替换整个树(); // 瞬间切换,没有中间状态 ✅
}
- 复用节点,节省内存
// 两棵树共享 DOM 节点
旧树.节点A.alternate = 新树.节点A // 指向同一个 <div>
// 如果节点没变化
新树.节点A = 旧树.节点A // 直接复用,不用新建
- 支持中断恢复
// Render 阶段可以在 workInProgress 树上中断
function renderWithTimeSlice() {
while (nextUnitOfWork && 没超时) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork) {
// 中断了,但没关系
// workInProgress 树只构建了一半
// current 树还在正常显示
requestIdleCallback(继续构建);
} else {
// 构建完成,可以切换了
commitRoot();
}
}
📊 两棵树的生命周期
| 阶段 | current 树 | workInProgress 树 |
|---|---|---|
| 初始渲染 | null | 正在构建 |
| 首次渲染后 | 树A (显示) | 树A (也指向相同) |
| 第一次更新 | 树A (显示) | 树B (构建中) |
| 更新完成 | 树B (显示) | 树A (备用) |
| 第二次更新 | 树B (显示) | 树A (构建中) |
| 更新完成 | 树A (显示) | 树B (备用) |
两棵树轮流当「展示树」和「构建树」,交替使用!
1. 为什么叫「双缓冲」?
- 源自图形学:两个缓冲区交替显示和绘制
- 防止屏幕撕裂、闪烁
2. 什么时候用到了双缓冲?
- Render 阶段:在 workInProgress 上工作
- Commit 阶段:交换 current 和 workInProgress
3. 性能收益?
- 用户无感知的更新
- 可中断渲染
- 节点复用
🎚️ 调度系统(Scheduler)
React 其内部会周期性调用 shouldYield() 询问浏览器是否需要让步,如果浏览器需要渲染动画、响应用户操作,React 会暂停自己的渲染工作。
如果让步:
pause → 等下再继续 → resume
🏎 lane 模型
在 Fiber 内部,每个更新会被标记成 lane:
00001 → 高优先级更新
00010 → 普通更新
00100 → transition
01000 → 空闲任务
…
比如来了一个点击事件(超高优先级):
pendingLanes = 00001
React 检查当前渲染是否比它慢:
- 若慢,立即 中断当前渲染
- 处理高优先级任务
- 再回来继续做之前没做完的
🎤 总结
- “Fiber 就是一个可以被切片、暂停、恢复的渲染引擎,它把每个组件拆成可调度的工作单元,并用 lane 模型管理优先级,让 React 能在高负载下保持流畅的用户交互。”
- “render 阶段可中断,commit 阶段不可中断,这是保证用户看到 UI 一致性的关键。”
- “双缓冲结构让 React 可以在后台构建新的 UI,再一次性替换出来。”
“时间切片让渲染不再阻塞用户输入。”