事件循环、宏任务与微任务:深入解析 JavaScript 执行机制

125 阅读5分钟

在现代前端开发中,JavaScript 的异步执行机制是开发者必须掌握的核心概念之一。尤其是事件循环(Event Loop)宏任务(Macrotask)微任务(Microtask) 的执行顺序,常常出现在面试题中,也直接影响代码的运行结果。本文将结合一段经典面试题代码和其执行流程图,深入剖析这些核心概念,并帮助你彻底理解 JavaScript 的异步执行机制。

🔍 一、引言:为什么这道题如此重要?

这是一道经典的前端面试题,几乎每年都会出现在大厂笔试或面试点中。它综合考察了:

  • JavaScript 的单线程特性
  • 事件循环(Event Loop)机制
  • 宏任务(Macrotask)与微任务(Microtask)的区别
  • Promise 和 setTimeout 的执行时机

掌握这道题,意味着你真正理解了 JavaScript 的“灵魂”。


🧠 二、核心概念回顾

1. 调用栈(Call Stack)

  • 存放当前正在执行的函数。
  • 执行完一个函数后,从栈顶弹出。

2. 宿主环境(Host Environment)

  • 浏览器或 Node.js 提供的运行环境。
  • 负责处理定时器、DOM 操作、网络请求等异步操作。
  • 将异步任务放入 宏任务队列 或 微任务队列

3. 任务队列

队列类型典型例子
宏任务队列MacrotasksetTimeoutsetIntervalI/OUI 渲染
微任务队列MicrotaskPromise.thenMutationObserverprocess.nextTick

4. 事件循环(Event Loop)

  • 不断检查调用栈是否为空。

  • 若为空,则:

    1. 微任务队列取出所有任务并执行(清空);
    2. 宏任务队列取出一个任务执行;
    3. 重复上述过程。

✅ 关键规则:

  • 微任务优先于宏任务
  • 每次宏任务结束后,必须先清空所有微任务,再进入下一个宏任务
  • 即使微任务后入队,也会在当前宏任务结束后立即执行

📊 三、代码分析

console.log(1)

setTimeout(() => {
  console.log(2)
  const p = new Promise(resolve => resolve(3))
    .then(result => console.log(result))
}, 0)

const p = new Promise(resolve => {
  setTimeout(() => {
    console.log(4)
  }, 0)
  resolve(5)
})

p.then(result => console.log(result))

const p2 = new Promise(resolve => resolve(6))
p2.then(result => console.log(result))

console.log(7)

🎯 四、执行流程详解(分步走)

我们结合以下结构图来分析:

[调用栈] ←→ [宿主环境(浏览器)][微任务队列][宏任务队列]

第一步:执行初始同步代码(宏任务)

这是整个脚本的第一个宏任务,属于“script”级别的宏任务。

  1. console.log(1) → 输出 1

  2. setTimeout(...) → 创建定时器,回调函数进入 宏任务队列

  3. new Promise(...) 构造函数执行:

    • 内部 setTimeout(...) → 又一个宏任务,进入 宏任务队列
    • resolve(5) → 触发 .then(),但回调是微任务,进入 微任务队列
  4. p.then(...) → 微任务,进入 微任务队列

  5. p2 = new Promise(...) → 同步执行,resolve(6)

  6. p2.then(...) → 微任务,进入 微任务队列

  7. console.log(7) → 输出 7

✅ 初始宏任务执行完毕,调用栈为空。

⚠️ 注意:虽然 p.thenp2.then 在代码中写在 console.log(7) 前面,但它们的回调不会在 7 之前执行!因为微任务要等当前宏任务完全结束才运行。

此时输出:

1
7

第二步:执行微任务队列(第一次)

微任务队列中有两个任务(按注册顺序):

  1. p.then(...) → console.log(5)
  2. p2.then(...) → console.log(6)

→ 输出:

5
6

✅ 微任务全部执行完毕,微任务队列清空。


第三步:执行下一个宏任务(第一个 setTimeout

从宏任务队列取出第一个任务:

console.log(2)
const p = new Promise(resolve => resolve(3))
 .then(result => console.log(result))
  • console.log(2) → 输出 2
  • Promise.resolve(3).then(...) → 新增一个微任务(打印3),进入 微任务队列

✅ 这个宏任务执行完毕。


第四步:执行新产生的微任务

微任务队列中有一个任务:

  • console.log(3) → 输出 3

第五步:执行最后一个宏任务(打印4的 setTimeout

从宏任务队列取出第二个任务:

1console.log(4)

→ 输出 4


✅ 最终输出顺序

1
7
5
6
2
3
4

🧩 五、关键知识点总结

知识点说明
初始 script 是宏任务整个脚本首次执行是一个宏任务,同步代码连续执行
微任务不打断同步代码即使 .then() 写在 console.log(7) 前面,也不会提前执行
微任务优先级更高每次宏任务结束后,先清空所有微任务,再进入下一个宏任务
Promise.then 是微任务不会阻塞主线程,但会在当前宏任务结束后立即执行
setTimeout 是宏任务即使延时为 0,也必须等待当前宏任务完成

🔄 六、事件循环图示解释

结合你提供的图示:

text
编辑
1[调用栈] ←→ [宿主环境(浏览器)]
23   [微任务队列]
45[宏任务队列]
  • 调用栈:执行当前函数;
  • 宿主环境:负责创建定时器、处理 DOM 等;
  • 宏任务队列:存放 setTimeoutsetInterval 等;
  • 微任务队列:存放 Promise.thenMutationObserver 等;
  • 事件循环:不断检查调用栈是否为空,若为空则先执行微任务,再执行宏任务。

🧪 七、如何验证?

你可以直接运行这段代码:

js
编辑
1console.log(1);
2setTimeout(() => {
3  console.log(2);
4  Promise.resolve(3).then(console.log);
5}, 0);
6new Promise(resolve => {
7  setTimeout(() => console.log(4), 0);
8  resolve(5);
9}).then(console.log);
10Promise.resolve(6).then(console.log);
11console.log(7);

实际输出

text
编辑
11
27
35
46
52
63
74

✅ 结语

这道题之所以难,是因为它考验你对 事件循环机制的深刻理解,尤其是:

  • 微任务与宏任务的执行顺序;
  • 同步与异步的边界;
  • Promise 与 setTimeout 的区别。

只要你记住一句话:

“宏任务执行完 → 清空所有微任务 → 再执行下一个宏任务”

就能轻松应对类似题目。

如果你还没完全理解,建议你画出执行流程图,一步步模拟每个任务的加入与执行。

🎯 小测验:你能预测下面这段代码的输出吗?

js
编辑
1console.log('start');
2Promise.resolve().then(() => console.log('promise1'));
3setTimeout(() => console.log('timeout1'), 0);
4Promise.resolve().then(() => console.log('promise2'));
5setTimeout(() => console.log('timeout2'), 0);
6console.log('end');

答案:start -> end -> promise1 -> promise2 -> timeout1 -> timeout2

✅ Tips

事件循环的执行过程以画图的形式最好理解:

image.png 能够清晰的展示微任务与宏任务的执行顺序,所以这里建议大家在理解代码的过程中画画图,实在不理解的这里也给大家推荐b站上面的一个视频:www.bilibili.com/video/BV192…