本篇我们看一下,React 从 v15 到 v16 的更新机制演进 ,重点是 递归同步更新 → 时间分片 + 可中断更新 的变化。
背景
1. React v15:递归同步更新
核心特点
-
React v15 的更新是 同步递归更新:
- 一旦触发状态更新(
setState
或 props 改变),React 会从 根节点开始递归遍历虚拟 DOM。 - 对比新旧虚拟 DOM,找到差异,然后更新真实 DOM。
- 一旦触发状态更新(
-
问题:
- 对于大型组件树,递归更新会阻塞主线程。
- 页面无法响应用户交互或动画,浏览器卡顿明显。
-
缺点:
- 无法中断渲染。
- 所有任务都是同步完成,要么成功更新,要么浏览器长时间无响应。
示意:
更新触发 → 递归 diff → 同步更新 DOM
(整个过程阻塞主线程)
2. React v16:Fiber 架构引入
核心目标
- 解决 v15 的 阻塞渲染问题。
- 支持 可中断异步渲染,让 React 更加流畅、响应用户操作。
Fiber 的引入
-
Fiber 是 React v16 的内部数据结构:
- 将组件更新拆分为 一个个小任务单元(fiber)。
- 每个 fiber 对应组件的一个节点。
-
Fiber 可以 暂停、恢复、优先级调度。
-
由此引入 时间分片(time slicing) 和 异步可中断渲染。
3. 时间分片与异步可中断渲染
时间分片(Time Slicing)
- 浏览器每帧约 16ms(60fps),React 将更新任务拆分成小块。
- 每次只执行部分 fiber,执行过程中会检查
shouldYield()
。 - 如果浏览器忙(主线程占用或高优先级任务),React 会 中断当前任务,等待下一帧继续。
- 这样用户交互、动画、滚动不会被阻塞。
异步可中断
-
v16 不再一次性递归整个树。
-
每个 fiber 是 可中断的单位。
-
Fiber 更新被分配 优先级:
- 高优先级任务(如用户输入)可立即打断低优先级更新。
- 低优先级任务(如数据渲染)可延后,浏览器空闲时再执行。
-
结合 Scheduler,React 实现:
- 同步更新 → 高优先级立即执行。
- 异步更新 → 根据空闲时间片分批执行,避免阻塞。
示意:
rust
更新触发 → 创建 fiber 树 → 分片执行 fiber → 可中断 / 恢复
|<--时间分片-->|<--时间分片-->| ...
4. v15 → v16 的本质变化对比
特性 | React v15 | React v16 (Fiber) |
---|---|---|
更新方式 | 同步递归更新 | 异步可中断更新(时间分片) |
线程占用 | 阻塞主线程 | 可让出主线程,浏览器渲染不卡顿 |
用户交互 | 大型更新阻塞用户操作 | 高优先级任务可打断低优先级任务 |
更新单位 | 整棵树 | fiber 单元(组件节点粒度) |
优先级管理 | 无 | 高/中/低/空闲优先级分级 |
效果 | 更新大时卡顿明显 | 大型更新可平滑分帧渲染,流畅性高 |
react的异步调度是如何实现的
React 的异步调度核心目标:解决浏览器卡顿问题,同时保证 UI 更新的流畅性。
1. 问题背景
从上面我们知道
- 为了保证用户体验,React 的更新过程是从 root 开始进行"diff",早期是同步的,会阻塞浏览器渲染。
- 浏览器一帧大约 16ms(60fps),如果 React 更新占满主线程,页面就会卡顿。
- 解决思路:将 React 更新任务交给浏览器空闲时间执行,让浏览器优先完成绘制任务,再执行不紧急的更新。
2. 时间分片(Time Slicing)
- 浏览器每帧执行顺序:
事件处理 → JS 执行 → requestAnimationFrame → Layout → Paint
- 空闲时间:如果事件循环结束且没有其他任务,浏览器进入休息状态,此时可以执行不紧急任务。
- React 利用这种空闲时间,分片执行更新任务。
3. 浏览器空闲时间 API
-
requestIdleCallback:浏览器提供的 API,在空闲时执行回调。
scssrequestIdleCallback(callback, { timeout })
callback
:空闲时执行timeout
:最大等待时间,防止长时间未执行
-
React 对任务设置五个优先级:
优先级 超时时间 用途说明 Immediate -1 立即执行 UserBlocking 250ms 用户交互相关 Normal 5000ms 普通更新,如网络请求 Low 10000ms 可延迟处理的任务 Idle ∞ 非必要任务,可能不执行
4. requestIdleCallback 兼容实现
由于 requestIdleCallback 仅 Chrome 支持,React 自己实现了兼容方案,需要满足两个条件:
- 能主动让出主线程,让浏览器去渲染视图。
- 每次事件循环只执行一次,任务递归请求下一次时间片。
5.为什么宏任务满足上面两个条件?
先回顾下浏览器事件循环
浏览器事件循环中有两类任务:
- 宏任务(MacroTask) :setTimeout、setInterval、setImmediate(Node)、MessageChannel、postMessage、I/O、UI 渲染。
- 微任务(MicroTask) :Promise.then、MutationObserver、queueMicrotask。
事件循环大致流程:
- 取一个宏任务执行。
- 清空所有微任务。
- 更新渲染(浏览器有机会去绘制 UI)。
- 回到步骤 1。
需求分析
我们要实现一个类似 requestIdleCallback 的 polyfill,要求:
- 主动让出主线程,让浏览器渲染视图 。
→ 也就是说,不能一直在本轮事件循环中耗时执行。
→ 要把任务放到下一次事件循环,这样浏览器中间就能去渲染。 - 一次事件循环只执行一次任务,执行后再请求下一次 。
→ 不希望和浏览器的渲染"抢时间"。
为什么只有"宏任务"能满足?
-
微任务不行 :
微任务会在当前宏任务结束后立刻执行 ,而且在浏览器渲染之前就被清空。
→ 如果用 Promise.then 来实现,会一直占用在同一轮循环里,浏览器根本没机会插入渲染。
→ 这就不符合"让出主线程"的要求。
-
宏任务可以:
- 宏任务之间,浏览器有机会去更新渲染。
- 比如用
setTimeout(fn, 0)
或者MessageChannel
来调度。 - 每次执行完宏任务后,事件循环会进入渲染阶段,满足了"浏览器有机会渲染"。
- 而且一次循环只执行一个宏任务,天然保证了"只执行一次"的条件。
举个例子
js
function myRequestIdleCallback(callback) {
return setTimeout(() => {
const start = Date.now();
// 给 callback 提供 deadline 参数,模拟 requestIdleCallback 的行为
callback({
didTimeout: false,
timeRemaining: () => Math.max(0, 50 - (Date.now() - start)) // 模拟剩余时间
});
}, 0);
}
这样做的效果:
- 用
setTimeout
(宏任务)调度 → 下一次事件循环执行,浏览器中间可以渲染。 - 每次执行完,再调度下一次 → 保证"一次循环只执行一次"。


✅ 所以结论是:
- 微任务太快,会阻塞渲染 → 不行
- 宏任务会让浏览器有机会渲染,而且一次循环只执行一次 → 符合条件
6. 宏任务方案的选择,为什么没选setTimeout
(1) setTimeout(fn, 0)
- 可以创建宏任务,不阻塞浏览器。
- 问题:递归执行时,间隔会变为约 4ms,而不是 1ms,效率低,浪费时间。我总共16ms的一次循环时间,你一下消费4ms,那肯定不行啊,react希望精细到1ms
(2) MessageChannel(React 的实现方案)
-
创建一个消息通道(MessageChannel),管道通信,一边发送另一边接收,通过
port.postMessage
异步通知执行任务。 -
流程:
- React 调用
requestHostCallback
注册更新任务到scheduledHostCallback
。 port2.postMessage(null)
发送消息通知port1
。port1.onmessage
被触发,执行scheduledHostCallback
。- 执行完后清空
scheduledHostCallback
。
- React 调用
-
优势:
- 延迟低,几乎每帧可立即执行。
- 不阻塞主线程,保证浏览器渲染流畅。
- 高效实现时间分片调度。
7. 总结
React 异步调度(Scheduler)核心思想:
- 时间分片:把更新拆分成小任务,一帧一帧执行。
- 空闲时间利用:浏览器有空闲才执行,保证用户体验。
- 优先级管理:不同任务根据重要性设定超时时间。
- 兼容实现:MessageChannel 替代 setTimeout,实现跨浏览器高效异步执行。
React 的调度选择牺牲了部分同步响应(不如 Vue 响应更快)来换取 页面流畅度和用户体验。