JavaScript事件循环机制(Event Loop)
发表于更新于
面试JavaScript事件循环机制(Event Loop)
OHNII核心流程(记忆关键)
记忆口诀:同步代码 → 微任务 → 宏任务 → 微任务 → 宏任务…
执行顺序:执行栈 → 微任务队列清空 → 取一个宏任务 → 微任务队列清空 → 循环
1. 基础概念
执行栈(Call Stack)
- 同步代码按顺序执行的地方
- 函数调用形成栈帧,先进后出(LIFO)
- 栈空时才会检查任务队列
任务队列(Task Queue)
- 宏任务队列(Macro Task):script、setTimeout、setInterval、I/O、UI渲染、postMessage
- 微任务队列(Micro Task):Promise.then/catch/finally、MutationObserver、process.nextTick(Node.js)
2. 事件循环完整流程
1 2 3 4 5 6 7 8 9 10 11
| 1. 执行同步代码(执行栈) ↓ 2. 执行栈清空 ↓ 3. 执行所有微任务(清空微任务队列) ↓ 4. 渲染更新(如果需要) ↓ 5. 取出一个宏任务执行 ↓ 6. 回到步骤2(循环)
|
关键点:
- 每次执行一个宏任务后,会清空所有微任务
- 微任务在当前宏任务结束后立即执行
- 微任务可以插队,宏任务不行
3. 宏任务 vs 微任务
宏任务(Macro Task)
1 2 3 4 5 6 7 8
| setTimeout(() => {}, 0) setInterval(() => {}, 0) setImmediate() requestAnimationFrame() I/O操作 UI渲染 script标签代码
|
微任务(Micro Task)
1 2 3 4 5 6 7 8
| Promise.then() Promise.catch() Promise.finally() async/await MutationObserver process.nextTick() queueMicrotask()
|
4. 经典面试题解析
题目1:基础执行顺序
1 2 3 4 5 6 7 8 9 10 11
| console.log('1')
setTimeout(() => { console.log('2') }, 0)
Promise.resolve().then(() => { console.log('3') })
console.log('4')
|
执行流程:
1 2 3 4 5 6
| 1. 执行同步代码:打印 1 2. 遇到setTimeout,放入宏任务队列 3. 遇到Promise.then,放入微任务队列 4. 执行同步代码:打印 4 5. 同步代码执行完,清空微任务队列:打印 3 6. 取出宏任务执行:打印 2
|
输出:1 → 4 → 3 → 2
题目2:多层嵌套
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| console.log('start')
setTimeout(() => { console.log('timeout1') Promise.resolve().then(() => { console.log('promise1') }) }, 0)
Promise.resolve().then(() => { console.log('promise2') setTimeout(() => { console.log('timeout2') }, 0) })
console.log('end')
|
执行流程:
1 2 3 4 5 6 7 8
| 1. 同步:打印 start 2. setTimeout1 → 宏任务队列 3. Promise.then → 微任务队列 4. 同步:打印 end 5. 清空微任务:打印 promise2,setTimeout2 → 宏任务队列 6. 取宏任务:打印 timeout1,Promise.then → 微任务队列 7. 清空微任务:打印 promise1 8. 取宏任务:打印 timeout2
|
输出:start → end → promise2 → timeout1 → promise1 → timeout2
题目3:async/await
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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')
|
关键理解:
1 2 3 4 5 6 7 8
| await async2() console.log('async1 end')
async2().then(() => { console.log('async1 end') })
|
执行流程:
1 2 3 4 5 6 7 8 9 10
| 1. 同步:打印 script start 2. setTimeout → 宏任务队列 3. 执行async1:打印 async1 start 4. 执行async2:打印 async2 5. await后代码 → 微任务队列(async1 end) 6. Promise构造函数同步执行:打印 promise1 7. Promise.then → 微任务队列 8. 同步:打印 script end 9. 清空微任务:打印 async1 end → promise2 10. 取宏任务:打印 setTimeout
|
输出:script start → async1 start → async2 → promise1 → script end → async1 end → promise2 → setTimeout
题目4:复杂综合题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| console.log('1')
setTimeout(() => { console.log('2') Promise.resolve().then(() => { console.log('3') }) }, 0)
new Promise((resolve) => { console.log('4') resolve() }).then(() => { console.log('5') setTimeout(() => { console.log('6') }, 0) }).then(() => { console.log('7') })
setTimeout(() => { console.log('8') Promise.resolve().then(() => { console.log('9') }) }, 0)
console.log('10')
|
输出:1 → 4 → 10 → 5 → 7 → 2 → 3 → 8 → 9 → 6
详细流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 第一轮: - 同步:1, 4, 10 - 微任务队列:[then1] - 宏任务队列:[timeout1, timeout2]
第二轮: - 清空微任务:5(then1执行,产生then2和timeout3) - 微任务队列:[then2] - 宏任务队列:[timeout1, timeout2, timeout3]
第三轮: - 清空微任务:7(then2执行) - 取宏任务:2(timeout1执行,产生promise) - 微任务队列:[promise]
第四轮: - 清空微任务:3 - 取宏任务:8(timeout2执行,产生promise) - 微任务队列:[promise]
第五轮: - 清空微任务:9 - 取宏任务:6(timeout3执行)
|
5. Node.js中的事件循环
Node.js事件循环6个阶段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ┌───────────────────────────┐ ┌─>│ timers │ (setTimeout, setInterval) │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ (I/O回调) │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ (内部使用) │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ poll │ (I/O轮询) │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ check │ (setImmediate) │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ (关闭回调) └───────────────────────────┘
|
Node.js微任务优先级
1 2
| process.nextTick() Promise.then()
|
Node.js vs 浏览器差异
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| setTimeout(() => { console.log('timeout1') Promise.resolve().then(() => { console.log('promise1') }) }, 0)
setTimeout(() => { console.log('timeout2') Promise.resolve().then(() => { console.log('promise2') }) }, 0)
|
6. 面试口述版本
面试官:请解释JavaScript的事件循环机制
回答框架:
“JavaScript是单线程语言,通过事件循环机制实现异步操作。
核心流程分为三步:
执行同步代码:所有同步代码在执行栈中按顺序执行
清空微任务队列:执行栈清空后,会立即执行所有微任务,包括Promise.then、async/await等
执行宏任务:从宏任务队列取出一个任务执行,比如setTimeout、setInterval
执行完一个宏任务后,会再次清空微任务队列,然后再取下一个宏任务,如此循环。
关键点:
- 微任务优先级高于宏任务
- 每次只执行一个宏任务,但会清空所有微任务
- 常见微任务:Promise.then、async/await
- 常见宏任务:setTimeout、setInterval、I/O
实际应用:
- 理解异步代码执行顺序
- 优化性能(微任务比宏任务快)
- 避免阻塞主线程”
7. 高频追问
Q1: 为什么微任务比宏任务先执行?
- 微任务是在当前宏任务执行完后立即执行,不需要等待
- 宏任务需要等待事件循环的下一轮
- 微任务可以更快地响应异步操作结果
Q2: Promise和setTimeout(0)谁先执行?
- Promise.then先执行(微任务)
- setTimeout后执行(宏任务)
- 即使setTimeout设置为0,也会在下一轮事件循环执行
Q3: async/await的执行顺序?
1 2 3 4 5 6 7 8 9 10 11 12
| async function test() { console.log('1') await Promise.resolve() console.log('2') }
function test() { console.log('1') Promise.resolve().then(() => { console.log('2') }) }
|
- await前的代码同步执行
- await后的代码作为微任务执行
Q4: 如何理解”JavaScript是单线程”?
- 主线程只有一个,同一时间只能执行一个任务
- 但浏览器是多线程的(渲染线程、HTTP请求线程、定时器线程等)
- 异步操作由其他线程处理,完成后回调放入任务队列
- 事件循环负责协调主线程和任务队列
Q5: requestAnimationFrame属于宏任务还是微任务?
- 既不是宏任务也不是微任务
- 在浏览器渲染之前执行
- 执行时机:微任务之后,渲染之前
- 适合做动画,因为与屏幕刷新率同步(60fps)
Q6: 如何避免事件循环阻塞?
- 避免长时间同步操作
- 使用Web Worker处理复杂计算
- 大任务拆分成小任务(时间切片)
- 使用requestIdleCallback在空闲时执行
- 合理使用防抖节流
8. 实战技巧
技巧1:快速判断执行顺序
1 2 3 4
| 1. 先找所有同步代码 2. 再找所有微任务(Promise.then、async/await) 3. 最后找宏任务(setTimeout、setInterval) 4. 注意嵌套关系,每执行完一个宏任务就清空微任务
|
技巧2:画图分析
1 2 3 4
| 执行栈 | 微任务队列 | 宏任务队列 ------|----------|---------- 同步1 | Promise1 | timeout1 同步2 | Promise2 | timeout2
|
技巧3:记住优先级
1
| 同步代码 > process.nextTick > Promise.then > setTimeout > setImmediate
|