React为减缓浏览器单帧渲染压力做了时间切片处理,然后通过宏任务进行调度,其底层基于浏览器的渲染 和事件循环 的原理。浏览器中的渲染帧 和事件循环是紧密协作但职责分明的两个核心机制,它们的关系可以通过以下要点清晰呈现:
1. 层级关系
plaintext
事件循环(Event Loop)
├─ 宏任务队列(Task Queue)
├─ 微任务队列(Microtask Queue)
└─ 渲染帧(Frame)
├─ 样式计算(Style)
├─ 布局(Layout)
└─ 绘制(Paint)
2. 协作流程
在 react/packages/scheduler/src/forks/Scheduler.js
中体现的协作逻辑:
javascript
function performWorkUntilDeadline() {
// 阶段1:执行宏任务(如React调度任务)
flushWork();
// 阶段2:清空微任务队列(浏览器内部行为)
// 阶段3:执行渲染管线(浏览器内部行为)
if(needsPaint) requestPaint();
}
标准事件循环迭代:
- 从宏任务队列取出一个任务执行
- 执行所有微任务(直到队列清空)
- 执行渲染管线(如果需要)
- 进入下一轮循环
3. 关键差异
特性 | 事件循环 | 渲染帧 |
---|---|---|
触发频率 | 持续运行 | 按屏幕刷新率触发(如60Hz) |
核心职责 | 任务调度与执行 | 视觉更新计算 |
可中断性 | 不可中断(必须执行到微任务清空) | 可跳过部分阶段 |
性能影响 | 长任务导致事件循环卡顿 | 复杂样式导致渲染耗时 |
4. React的协调策略
React调度器通过时间切片主动适配两者:
javascript
function workLoop() {
while (task && !shouldYield()) {
process(task); // 每次执行不超过5ms
}
// 主动让出控制权给浏览器
if (hasMoreWork) schedulePerformWorkUntilDeadline();
}
这样设计使得:
- 事件循环不会被长时间阻塞
- 渲染帧能获得稳定的执行机会
- 高优先级更新能及时反映到视图
5. 实际案例对比
原生事件循环:
javascript
// 案例1:阻塞型微任务
Promise.resolve().then(() => {
heavyTask(50ms); // 直接导致3帧丢失
});
React调度:
javascript
// 案例2:时间切片
function task() {
chunkedWork(5ms); // 分块执行
if (remainingWork) {
scheduleTask(task); // 下一宏任务继续
}
}
总结关系图
plaintext
[事件循环] ← 控制权交替 → [渲染帧]
↑ ↑
│ │
宏任务/微任务 样式/布局/绘制
│ │
└─── React调度器协调 ───┘
这种设计就像双人接力赛跑:
- 事件循环是持棒的奔跑者(必须持续前进)
- 渲染帧是交棒检查点(按固定节奏接棒)
- React调度器是教练(控制交接时机和节奏)
问题
问:如果React代码中有比执行较长时间的微任务呢?也能保持流畅吗?
答:当然是否定。因为React对宏任务做了时间切片,微任务在渲染之前执行,如果时间过长,仍然会阻塞渲染。