事件循环 Event Loop 面试题全解析
一、核心要点速览
💡 核心考点
- 执行栈: 同步任务的执行环境(LIFO)
- 任务队列: 异步任务的等待队列(FIFO)
- 宏任务: setTimeout、setInterval、I/O 等
- 微任务: Promise.then、MutationObserver 等
- 执行顺序: 同步 → 微任务 → 宏任务
二、为什么需要事件循环?
1. 单线程的困境
JavaScript 是单线程语言,同一时间只能执行一个任务。如果遇到耗时操作,界面就会卡死。
2. 事件循环的解决方案
将耗时任务丢给后台处理,先注册回调,等主线程空闲再取出来执行。
3. 核心优势
- ✅ 非阻塞 I/O
- ✅ 高并发能力
- ✅ 良好的用户体验
三、核心概念:调用栈与任务队列
| 概念 | 名称 | 特点 | 存储内容 |
|---|---|---|---|
| 调用栈 | Call Stack | 同步执行,LIFO | 正在执行的任务 |
| 宏任务队列 | Macrotask Queue | 每次事件循环执行一个 | setTimeout、setInterval、I/O |
| 微任务队列 | Microtask Queue | 每轮循环清空所有 | Promise.then、queueMicrotask |
执行顺序
同步代码执行 → 微任务全部执行 → 渲染(如需要)→ 一个宏任务执行 → 回到循环四、宏任务 vs 微任务
1. 快速对比
| 特性 | 宏任务 | 微任务 |
|---|---|---|
| 包含 | setTimeout、setInterval、I/O、UI渲染 | Promise.then、queueMicrotask、MutationObserver |
| 执行数量 | 每次一个 | 全部清空 |
| 优先级 | 低 | 高 |
2. 执行口诀
同步代码最先跑,微任务紧接着,宏任务最后到!
五、经典面试题
题目 1:基础顺序
text
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
// 输出:1 → 4 → 3 → 2
// 解析:同步(1,4) → 微任务(3) → 宏任务(2)题目 2:async/await 嵌套
text
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() { console.log('async2') }
console.log('script start')
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题目 3:复杂嵌套
text
console.log('A')
setTimeout(() => {
console.log('B')
Promise.resolve().then(() => console.log('C'))
}, 0)
Promise.resolve().then(() => {
console.log('D')
setTimeout(() => console.log('E'), 0)
})
console.log('F')
// 输出:A → F → D → B → C → E
// 解析:同步(A,F) → 微(D) → 宏(B) → 微(C) → 宏(E)六、Node.js 事件循环
六阶段详解
| 阶段 | 名称 | 负责处理 | 典型 API |
|---|---|---|---|
| 1 | timers | 执行 setTimeout/setInterval 回调 | setTimeout |
| 2 | pending callbacks | 推迟的 I/O 回调(TCP 错误等) | TCP |
| 3 | idle, prepare | 内部调度 | 内部使用 |
| 4 | poll(核心) | 等待 I/O 完成并执行回调 | readFile |
| 5 | check | 执行 setImmediate 回调 | setImmediate |
| 6 | close callbacks | 执行 close 事件回调 | socket.on('close') |
特殊队列优先级
process.nextTick(最高)> Promise.then > 六阶段process.nextTick 示例
javascript
// Node.js 中:process.nextTick 优先级最高
console.log('start')
process.nextTick(() => {
console.log('nextTick 1')
process.nextTick(() => {
console.log('nextTick 2')
})
})
Promise.resolve().then(() => {
console.log('promise')
})
console.log('end')
// Node.js 输出:
// start
// end
// nextTick 1
// nextTick 2
// promise
// 浏览器输出(没有 nextTick):
// start
// end
// promisesetTimeout vs setImmediate
- 一般情况:执行顺序不确定(取决于性能)
- I/O 回调中:setImmediate 总是先于 setTimeout 执行
七、实际应用
1. 批量 DOM 更新
使用微任务批量处理,可以将多次 DOM 更新合并为一次,减少重排重绘。
2. 延迟执行优化
- setTimeout: ~4ms 延迟
- queueMicrotask: ~0.1ms 延迟
3. 框架状态更新
Vue/React 的状态更新机制都基于微任务实现。
八、面试标准回答
事件循环(Event Loop)是 JavaScript 处理异步任务的机制,使得单线程的 JS 能够高效处理大量并发操作。
核心概念:
- 调用栈:执行同步代码,LIFO
- 任务队列:存储异步回调,分宏任务和微任务
- 执行顺序:同步 → 微任务 → 宏任务
宏任务 vs 微任务:
- 宏任务每次执行一个,微任务每轮清空
- 微任务优先级高于宏任务
async/await:await 后的代码作为微任务执行
Node.js 特殊点:process.nextTick 优先级最高,setImmediate 在 I/O 回调中总是先于 setTimeout 执行
九、记忆口诀
JS 是单线程,
Event Loop 来处理。
调用栈里放同步,
任务队列存异步!
微任务优先级高,
Promise 和 queueMicro。
宏任务在后面等,
setTimeout 和 I/O!
执行顺序要记牢:
同步代码最先跑,
微任务今天讲,
宏任务最后到!十、推荐资源
十一、总结
- Event Loop: 调用栈 + 任务队列 = 异步编程核心机制 🔄
- 宏任务 vs 微任务: 优先级不同 = 正确的执行顺序 ⚡
- async/await: 同步写法 + 微任务实现 = 异步编程最佳实践 ✓