本文参考卡颂老师的《React 技术揭秘》,并结合小dora个人理解与源码阅读编写的一篇博客。
目标是让你看懂:React 为什么要重写架构、Fiber 到底解决了什么问题。
一、React15:一个"全力以赴但不会刹车"的系统
React15 的架构只有两层:
- 🧩 Reconciler(协调器) :负责计算哪些组件要更新;
- 🖼️ Renderer(渲染器) :把更新同步到对应平台(浏览器、原生、测试环境等)。
听起来没问题,但问题出在它的更新策略 ------
React15 在更新时使用的是递归调用。
每次调用 setState()
时,React 会自上而下递归遍历整棵组件树。
我们可以用伪代码看看它的本质:
scss
function updateComponent(component) {
component.render(); // 渲染当前组件
component.children.forEach(updateComponent); // 递归子组件
}
简单粗暴,效率直接。
但问题是------一旦递归开始,就停不下来。
🧠 举个例子:
假设你有一棵很深的组件树,当用户点击按钮触发更新时,
React 就会一路递归更新下去:
less
App
├─ Header
├─ Main
│ ├─ List
│ │ ├─ Item #1
│ │ ├─ Item #2
│ │ └─ Item #3
│ └─ Sidebar
└─ Footer
当层级很深、每个组件都要执行 render()
时,
整个递归过程会持续超过 16ms(一帧的理想渲染时间)。
这意味着在更新的过程中,浏览器完全没有机会响应用户操作:
想点击?等我更新完再说。
想输入?我还在 render 呢。
这,就是 React15 最大的痛点------同步更新不可中断。
二、如果在中途强行"打断"会发生什么?
假设我们有个 Demo:
javascript
function List({ items }) {
return (
<ul>
{items.map((num) => (
<li key={num}>{num * 2}</li>
))}
</ul>
);
}
用户希望看到 [1, 2, 3] → [2, 4, 6]
。
如果中途在更新到第二个 <li>
时被中断,就可能出现半成品页面:
css
<li>2</li>
<li>2</li>
<li>3</li>
React15 没法处理这种情况。因为它没有保存中间状态,也没有"恢复机制"。
它只能一口气跑完。
这时候 React 团队意识到:
我们需要一个可以「暂停、恢复、甚至丢弃」任务的架构。
三、React16:Fiber------让 React 学会「调度」
于是,在 React16 中,React 团队重写了整个协调层,设计了新的架构:
diff
+------------------+
| Scheduler | 调度器:分配优先级,安排执行顺序
+------------------+
| Reconciler | 协调器:找出变化的组件(Fiber)
+------------------+
| Renderer | 渲染器:将变化反映到宿主环境
+------------------+
新增的那一层 Scheduler(调度器) 就是关键!
🧬 Fiber 是什么?
简单来说,Fiber 是对「组件更新单元」的抽象 。
每个组件都会对应一个 Fiber 对象,它保存:
yaml
{
type: Component,
pendingProps: newProps,
child: firstChildFiber,
sibling: nextFiber,
return: parentFiber
}
它就像是一个链表节点,连接整棵组件树。
通过 Fiber,React 可以记录任务执行的进度。
🔁 可中断的循环
React16 的更新逻辑不再是递归,而是循环:
scss
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
每次只处理一个 Fiber 单元,然后问一句:
scss
if (shouldYield()) pause();
shouldYield()
就是核心判断:
👉 当前帧的时间是否用完?
👉 有没有更高优任务进来?
如果答案是"是",就中断执行,把控制权交还给浏览器。
React 会在下一帧或空闲时间里继续从中断点恢复。
四、Scheduler:React 的「时间管理大师」
Fiber 可以被打断,但谁来决定打断时机?
这就轮到 Scheduler 登场了。
浏览器有个原生 API requestIdleCallback()
,
可以在浏览器空闲时执行任务,但它兼容性和触发频率都不稳定。
于是 React 自己实现了一个更强的版本:
📦
scheduler
包它模拟浏览器空闲回调,并为任务赋予多种优先级。
每个任务都带有权重,比如:
优先级 | 说明 | 示例 |
---|---|---|
Immediate | 立即执行 | 错误边界恢复 |
UserBlocking | 用户输入 | 输入框响应 |
Normal | 常规更新 | 列表渲染 |
Low | 低优任务 | 动画或日志 |
Idle | 空闲任务 | 后台预加载 |
通过这种优先级机制,React 终于可以像操作系统一样分配 CPU 时间。
五、渲染:内存标记 + 批量提交
Fiber 负责协调,Renderer 才是执行者。
在 React16 中,Reconciler 不再边遍历边渲染,而是先打标记、后统一提交。
比如:
ini
export const Placement = 0b0000000000010;
export const Update = 0b0000000000100;
export const Deletion = 0b0000000001000;
每个 Fiber 节点在内存中被打上这些标签。
等所有标记完成后,Renderer 一次性提交所有 DOM 变更。
这就保证了即使中途被中断,DOM 始终保持一致性。
六、可视化理解:React15 vs React16
对比项 | React15 | React16 (Fiber) |
---|---|---|
架构层次 | Reconciler + Renderer | Scheduler + Reconciler + Renderer |
更新机制 | 递归 | 循环 |
可中断性 | ❌ 不可中断 | ✅ 可中断 |
DOM 一致性 | 更新中可能闪烁 | 内存标记后统一提交 |
优先级调度 | 无 | 有(Scheduler) |
源码模块 | ReactDOM | react-reconciler + scheduler |
📊 可以把这两者比喻成:
- React15:单线程跑完一场马拉松,中途谁也拦不住;
- React16:多任务分片执行,随时暂停、恢复、插队。
七、总结:从渲染引擎到时间调度系统
React16 的架构重写并非简单的性能优化,
而是一种"调度哲学的引入"。
React 不再只是「渲染 DOM 的库」,
而是一个「管理任务优先级的调度系统」。
Fiber 让任务可中断;
Scheduler 让任务有先后;
Renderer 让任务有结果。
React 的底层逻辑已经从:
同步执行 → 异步调度
演化成一套"以用户体验为核心的调度架构"。
📘 参考资料
- 卡颂,《React 技术揭秘》
- React 官方源码(react-reconciler / scheduler)
- React 团队公开设计文档