事件循环与任务队列
JavaScript 是单线程语言,但能处理异步操作——这一切都依赖事件循环(Event Loop)机制。理解它是掌握 JS 异步编程的基础。
执行模型概览
┌─────────────────────────────────────────┐
│ Call Stack(调用栈) │
│ [ main() → setTimeout cb → ... ] │
└──────────────────┬──────────────────────┘
│ 栈空时取任务
┌──────────▼──────────┐
│ Event Loop │
└──────────┬──────────┘
┌─────────┴──────────┐
▼ ▼
Microtask Queue Macrotask Queue
(微任务队列) (宏任务队列)
- Promise.then - setTimeout
- queueMicrotask - setInterval
- MutationObserver - setImmediate (Node)
- I/O callbacks
- requestAnimationFrame执行顺序规则
每轮 Event Loop 的执行顺序:
- 执行当前调用栈中的同步代码(直到栈空)
- 清空所有微任务(Microtask Queue)
- 取一个宏任务(Macrotask)执行
- 再次清空所有微任务
- 重复 3-4
关键点
微任务队列会在每个宏任务执行完后完全清空,然后才执行下一个宏任务。
经典面试题解析
js
console.log('1') // 同步
setTimeout(() => console.log('2'), 0) // 宏任务
Promise.resolve()
.then(() => console.log('3')) // 微任务
.then(() => console.log('4')) // 微任务
console.log('5') // 同步
// 输出顺序:1 → 5 → 3 → 4 → 2执行过程分解:
同步阶段:
console.log('1') ✓
setTimeout 注册到宏任务队列
Promise.then 注册到微任务队列
console.log('5') ✓
微任务清空:
console.log('3') ✓
.then(() => '4') 注册到微任务队列
console.log('4') ✓
宏任务:
console.log('2') ✓async/await 的本质
async/await 是 Promise 的语法糖,await 后面的代码等价于 .then() 回调:
js
async function foo() {
console.log('A')
await Promise.resolve()
console.log('B') // 等价于 Promise.resolve().then(() => console.log('B'))
await Promise.resolve()
console.log('C')
}
console.log('start')
foo()
console.log('end')
// 输出:start → A → end → B → C更复杂的例子
js
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end') // 微任务
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(() => console.log('setTimeout'), 0) // 宏任务
async1()
new Promise(resolve => {
console.log('promise1')
resolve()
}).then(() => console.log('promise2')) // 微任务
console.log('script end')
// 输出:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end ← 微任务
// promise2 ← 微任务
// setTimeout ← 宏任务Node.js 的事件循环
Node.js 的事件循环基于 libuv,比浏览器多了几个阶段:
┌───────────────────────────┐
┌─>│ timers │ ← setTimeout / setInterval
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ ← I/O 错误回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ ← 内部使用
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ ← 获取新 I/O 事件
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ ← setImmediate
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘process.nextTick vs Promise.then
js
// Node.js 中,process.nextTick 优先级高于 Promise.then
Promise.resolve().then(() => console.log('Promise'))
process.nextTick(() => console.log('nextTick'))
// 输出:nextTick → Promise
// nextTick 有独立的队列,在微任务队列之前执行setImmediate vs setTimeout
js
// 在 I/O 回调中,setImmediate 总是先于 setTimeout
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))
})
// 输出:immediate → timeout(在 I/O 回调中是确定的)实战:避免阻塞事件循环
js
// ❌ 错误:长时间同步计算阻塞事件循环
function heavyCompute(n) {
let result = 0
for (let i = 0; i < n; i++) {
result += Math.sqrt(i)
}
return result
}
// ✅ 正确:分片执行,让出控制权
function heavyComputeAsync(n, chunkSize = 10000) {
return new Promise(resolve => {
let result = 0
let i = 0
function chunk() {
const end = Math.min(i + chunkSize, n)
while (i < end) {
result += Math.sqrt(i++)
}
if (i < n) {
setTimeout(chunk, 0) // 让出事件循环
} else {
resolve(result)
}
}
chunk()
})
}微任务泄漏
js
// ⚠️ 危险:无限微任务会饿死宏任务
function infiniteMicrotask() {
Promise.resolve().then(infiniteMicrotask)
}
infiniteMicrotask()
// setTimeout 的回调永远不会执行!
// ✅ 正确:用 setTimeout 打断
function safeRecursive() {
setTimeout(safeRecursive, 0) // 宏任务,不会饿死其他任务
}可视化工具
推荐使用 Loupe 可视化观察 Event Loop 的执行过程。
总结
- 同步代码 → 微任务(Promise/nextTick)→ 宏任务(setTimeout/I/O)
- 每个宏任务执行完后,清空所有微任务
async/await本质是 Promise,await后的代码是微任务- Node.js 的
process.nextTick优先级高于Promise.then