事件循环详解
JavaScript事件循环深度解析:宏任务与微任务优先级、浏览器与Node.js差异。
1. 事件循环模型
1.1 浏览器事件循环
┌───────────────────────────┐
│ Call Stack │
│ ┌─────────────────────┐ │
│ │ 执行上下文 (LIFO) │ │
│ └─────────────────────┘ │
└──────────┬────────────────┘
│
┌─────┴──────┐
│ Event Loop │ ← 持续检查
└─────┬──────┘
│
┌───────┴────────┐
│ │
┌──┴───┐ ┌───┴────┐
│ Micro │ │ Macro │
│ task │ │ task │
│ Queue │ │ Queue │
└──────┘ └────────┘
1.2 执行流程
1. 执行全局同步代码(宏任务)
2. 执行栈清空后,检查微任务队列
3. 清空所有微任务(包括执行中新增的微任务)
4. 取一个宏任务执行
5. 重复步骤 2-4
2. 宏任务与微任务
2.1 分类
| 宏任务(Macro Task) | 微任务(Micro Task) |
|---|---|
<script> 整体代码 | Promise.then/catch/finally |
setTimeout / setInterval | MutationObserver |
setImmediate(Node.js) | queueMicrotask() |
| I/O 回调 | process.nextTick(Node.js) |
| UI 渲染 | IntersectionObserver |
requestAnimationFrame |
2.2 优先级
微任务优先级(从高到低):
process.nextTick(Node.js)
Promise.then
queueMicrotask
MutationObserver
宏任务优先级(从高到低):
setImmediate(Node.js)
setTimeout / setInterval
I/O
setImmediate(浏览器,部分支持)
2.3 经典面试题
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => console.log('5'), 0);
});
console.log('6');
// 输出顺序:1, 6, 4, 2, 3, 5
解析:
第一轮(宏任务:全局代码):
同步: 1, 6
微任务队列: [Promise.then → 4]
宏任务队列: [setTimeout → 2]
第二轮(清空微任务):
执行: 4
新增宏任务: [setTimeout → 5]
微任务队列: 空
第三轮(下一个宏任务: setTimeout → 2):
同步: 2
新增微任务: [Promise.then → 3]
第四轮(清空微任务):
执行: 3
第五轮(下一个宏任务: setTimeout → 5):
同步: 5
3. 微任务的递归问题
3.1 微任务饥饿
// 微任务递归会阻塞宏任务
function recursiveMicrotask() {
Promise.resolve().then(recursiveMicrotask);
}
recursiveMicrotask();
// setTimeout 永远无法执行!
// 使用 setTimeout 让出执行权
function recursiveTask() {
setTimeout(recursiveTask, 0);
}
recursiveTask();
// 其他任务有机会执行
3.2 微任务数量控制
async function processLargeArray(items) {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
// 每 100 项让出一次执行权
if (i % 100 === 0) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}
4. Node.js 事件循环
4.1 阶段模型
┌──────────────────────────┐
┌─>│ timers │ ← setTimeout/setInterval
│ └─────────────┬────────────┘
│ ┌─────────────┴────────────┐
│ │ pending callbacks │ ← 系统级回调(TCP 错误等)
│ └─────────────┬────────────┘
│ ┌─────────────┴────────────┐
│ │ idle, prepare │ ← 内部使用
│ └─────────────┬────────────┘
│ ┌─────────────┴────────────┐
│ │ poll │ ← I/O 回调
│ └─────────────┬────────────┘
│ ┌─────────────┴────────────┐
│ │ check │ ← setImmediate
│ └─────────────┬────────────┘
│ ┌─────────────┴────────────┐
│ │ close callbacks │ ← socket.on('close')
│ └─────────────┬────────────┘
│ │
└────────────────┘
4.2 process.nextTick vs Promise
// Node.js 中 process.nextTick 优先于 Promise
setImmediate(() => console.log('1: setImmediate'));
setTimeout(() => console.log('2: setTimeout'), 0);
Promise.resolve().then(() => console.log('3: Promise'));
process.nextTick(() => console.log('4: nextTick'));
// 输出: 4, 3, 2, 1
4.3 setTimeout vs setImmediate
// 在非 I/O 上下文中,顺序不确定
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 可能 timeout 先,也可能 immediate 先
// 在 I/O 回调中,setImmediate 一定先
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// 一定输出: immediate, timeout
5. requestAnimationFrame
5.1 执行时机
requestAnimationFrame 在每一帧渲染前执行,位于微任务之后、渲染之前:
宏任务 → 微任务(全部清空)→ requestAnimationFrame → 渲染
5.2 与 setTimeout 的区别
// setTimeout 可能丢帧
function animate() {
moveElement();
setTimeout(animate, 16); // 不保证与屏幕刷新同步
}
// rAF 与屏幕刷新同步
function animate() {
moveElement();
requestAnimationFrame(animate); // 在下一帧渲染前执行
}
6. 实践建议
6.1 任务拆分
// 长任务拆分为多个微任务
async function chunkedProcess(data) {
const CHUNK_SIZE = 100;
for (let i = 0; i < data.length; i += CHUNK_SIZE) {
const chunk = data.slice(i, i + CHUNK_SIZE);
processChunk(chunk);
if (i + CHUNK_SIZE < data.length) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}
6.2 优先级调度
// 高优先级任务用微任务
function urgentUpdate(data) {
queueMicrotask(() => render(data));
}
// 低优先级任务用宏任务
function backgroundSync() {
setTimeout(() => syncToServer(), 0);
}