Skip to content

事件循环 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 事件循环

Node.js Event Loop 六阶段详解

六阶段详解

阶段名称负责处理典型 API
1timers执行 setTimeout/setInterval 回调setTimeout
2pending callbacks推迟的 I/O 回调(TCP 错误等)TCP
3idle, prepare内部调度内部使用
4poll(核心)等待 I/O 完成并执行回调readFile
5check执行 setImmediate 回调setImmediate
6close 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
// promise

setTimeout vs setImmediate

  • 一般情况:执行顺序不确定(取决于性能)
  • I/O 回调中:setImmediate 总是先于 setTimeout 执行

七、实际应用

1. 批量 DOM 更新

使用微任务批量处理,可以将多次 DOM 更新合并为一次,减少重排重绘。

2. 延迟执行优化

  • setTimeout: ~4ms 延迟
  • queueMicrotask: ~0.1ms 延迟

3. 框架状态更新

Vue/React 的状态更新机制都基于微任务实现。


八、面试标准回答

事件循环(Event Loop)是 JavaScript 处理异步任务的机制,使得单线程的 JS 能够高效处理大量并发操作。

核心概念

  1. 调用栈:执行同步代码,LIFO
  2. 任务队列:存储异步回调,分宏任务和微任务
  3. 执行顺序:同步 → 微任务 → 宏任务

宏任务 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: 同步写法 + 微任务实现 = 异步编程最佳实践
最近更新