题记:React 的每一次渲染决策,都发生在事件循环的缝隙里。
🔍 本节要点
- 事件循环是 JavaScript 并发模型的唯一实现方式
- 任务队列与微任务队列的优先级差异
- 事件循环如何影响 React 的渲染时机
单线程的 JavaScript 怎么"同时"做很多事?
JavaScript 是单线程的——同一时刻只能执行一段代码。但我们的页面却能响应点击、播放动画、发送网络请求……这背后靠的就是事件循环(Event Loop)。
事件循环不是什么魔法,它就是一个永不停止的 while 循环,它的工作逻辑极其简单:
while (true) {
// 1. 执行完当前调用栈的所有同步代码
// 2. 清空微任务队列(全部执行完,包括过程中新增的微任务)
// 3. 尝试渲染(如果有待渲染的更新)
// 4. 从宏任务队列取一个任务,执行它
// 5. 重复
}
简单来说:事件循环就是不断地问——"现在谁在排队?"
两个队列的博弈
理解事件循环,关键在于理解两个队列:
宏任务队列(MacroTask Queue)
setTimeout、setInterval- I/O 操作
requestAnimationFrame- UI 渲染
微任务队列(MicroTask Queue)
Promise.then()/Promise.catch()queueMicrotask()MutationObserver
执行顺序的黄金法则:每次从宏任务队列取任务之前,必须清空整个微任务队列。
┌──────────────────────────────────────────────┐
│ 执行一个宏任务 │
│ (比如 setTimeout 的回调) │
└─────────────────┬──────────────────────────┘
│ 该任务执行完毕
┌─────────────────▼──────────────────────────┐
│ 清空微任务队列 │
│ (Promise.then, queueMicrotask...) │
│ 只要微任务队列非空就一直执行直到空 │
└─────────────────┬──────────────────────────┘
│ 微任务队列空了
┌─────────────────▼──────────────────────────┐
│ 尝试渲染(可能发生) │
└─────────────────┬──────────────────────────┘
│
┌─────────┴──────────┐
│ │
还有宏任务? 没有宏任务?
│ │
▼ ▼
取下一个宏任务 等待新任务
一个经常被面试题"用烂"的经典例子
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
Promise.resolve().then(() => {
setTimeout(() => console.log('4'), 0);
console.log('5');
});
console.log('6');
// 输出顺序:1 → 6 → 3 → 5 → 2 → 4
很多人死记硬背这个顺序,但其实理解了就很简单:
1和6是同步代码,直接执行setTimeout是宏任务 → 排队Promise.then是微任务 → 排队- 同步代码执行完毕,开始清空微任务队列:
3→5(5里的setTimeout又入宏任务队列) - 微任务队列空了,取下一个宏任务:
2 2执行过程中,触发了新的setTimeout→ 入队- 没有更多宏任务了,但
4那个定时器还在路上……
React 18 的自动批量(Batching)
在 React 18 之前,批量更新(batching)是有条件的:只有在 React 的事件处理函数内才会批量更新。如果你写了一个 setTimeout,里面的 setState 每次都会单独触发一次渲染——这叫"不必要的重新渲染"。
// React 17 行为
setTimeout(() => {
setCount(1); // 触发一次渲染
setFlag(true); // 再触发一次渲染 ❌ 浪费
}, 0);
// React 18 行为
setTimeout(() => {
setCount(1); // 排队
setFlag(true); // 合并,最终只触发一次渲染 ✅
}, 0);
React 18 的 automatic batching 改变了这个游戏规则:无论在哪个上下文中(setTimeout、Promise、fetch 回调),多次 setState 都会被合并为一次渲染。
事件循环与 React 渲染的交汇点
事件循环的"渲染时机"是一个很多人忽视的细节。事件循环执行完微任务后,可能会触发一次渲染——但不是每次都会。
决定是否渲染的因素包括:
- 是否有待提交的 DOM 更新
- 浏览器的渲染时机(通常 60fps,即每 16.67ms 一次)
requestAnimationFrame的调度
React 的 scheduler 模块正是在这个时间缝隙里工作的。它判断主线程是否足够空闲,以便继续执行 React 的工作单元(fiber)。
本节小结
事件循环是 JavaScript 世界的基础设施。理解它的循环模型、两个队列的优先级,对理解 React 的调度机制至关重要。
当你下次在 DevTools 里看到"长任务(Long Task)"警告时,你会知道:这是某个宏任务执行时间太长,堵住了事件循环,让页面无法渲染。