JavaScript事件循环机制(Event Loop)

核心流程(记忆关键)

记忆口诀:同步代码 → 微任务 → 宏任务 → 微任务 → 宏任务…

执行顺序:执行栈 → 微任务队列清空 → 取一个宏任务 → 微任务队列清空 → 循环


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() // Node.js
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() // Node.js,优先级最高
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后面的代码相当于
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)

// 浏览器输出:timeout1 → promise1 → timeout2 → promise2
// Node.js 11+:timeout1 → promise1 → timeout2 → promise2
// Node.js 10-:timeout1 → timeout2 → promise1 → promise2

6. 面试口述版本

面试官:请解释JavaScript的事件循环机制

回答框架:

“JavaScript是单线程语言,通过事件循环机制实现异步操作。

核心流程分为三步:

  1. 执行同步代码:所有同步代码在执行栈中按顺序执行

  2. 清空微任务队列:执行栈清空后,会立即执行所有微任务,包括Promise.then、async/await等

  3. 执行宏任务:从宏任务队列取出一个任务执行,比如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