JavaScript 事件循环彻底搞懂:微任务与宏任务面试必问

zero
zero 见习用户
发布于 2026-09-22 14:43 ·4 浏览 ·0 回复

学完这篇你能得到:拿到任何一段混着 `setTimeout`、`Promise`、`async/await` 的代码,都能逐行说出输出顺序,被追问「为什么」也答得上来。

第一步:先建立心智模型

JavaScript 是单线程的,同一时刻只能执行一段代码。它靠三个东西协作:

  • 调用栈(Call Stack):正在执行的同步代码,后进先出。
  • 任务队列(Task Queue):异步任务完成后,回调排在这里等着。
  • 事件循环(Event Loop):不停检查「栈空了没」,空了就从队列里取任务压栈。

关键点是:任务队列不是一个,是两个——宏任务队列和微任务队列。这是所有面试题的题眼。

第二步:分清宏任务和微任务各有哪些

宏任务(macrotask):整个 `<script>` 脚本本身、`setTimeout`、`setInterval`、`setImmediate`(Node)、I/O 操作、`MessageChannel`、用户交互事件。

微任务(microtask):`Promise.then / catch / finally`、`queueMicrotask()`、`MutationObserver`、`async/await` 中 `await` 之后的代码、Node 里的 `process.nextTick`。

注意:`Promise` 的构造函数里的代码是同步执行的,只有 `.then` 里的回调才是微任务。很多人栽在这一点上。

第三步:背下这条调度规则

事件循环每一轮的顺序固定是:

  1. 执行同步代码,直到调用栈清空;
  2. 清空整个微任务队列(注意:微任务执行时新产生的微任务,也在本轮一起清掉,直到队列彻底空);
  3. 浏览器判断是否需要渲染(可能在这一步重绘);
  4. 从宏任务队列取出一个宏任务执行;
  5. 回到第 2 步。

一句话记住:宏任务一次取一个,微任务一次清一空。 微任务的优先级永远高于下一个宏任务。

第四步:跑一遍最经典的例子

console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')

输出:`1 4 3 2`。

过程:`1`、`4` 是同步代码直接出;`setTimeout` 的回调被扔进宏任务队列;`.then` 的回调扔进微任务队列;同步代码跑完,先清微任务输出 `3`;再取宏任务输出 `2`。

第五步:搞定 async/await

把 `await` 理解成「一个 `.then` 的语法糖」就不会错:

async function f() {
  console.log('a')
  await null
  console.log('b')
}

`await` 之前的代码同步执行(输出 `a`),`await` 之后的代码被包进微任务(输出 `b`)。

注意:`await` 后面跟一个已经 resolve 的 Promise,仍然会让出主线程一次,后面的代码照样进微任务队列。别以为会「立刻继续执行」。

第六步:Node 与浏览器的差异

  • Node 里 `process.nextTick` 有独立队列,优先级高于 Promise 微任务。
  • Node 11 之前,微任务只在各阶段之间清空;Node 11 之后行为与浏览器基本对齐,每个宏任务执行完就清微任务。
  • 浏览器有渲染,Node 没有。所以微任务里写死循环,浏览器会直接卡死白屏。

第七步:一道综合题

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')

输出:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

拆解:`script start` 同步;`async1()` 调用后同步输出 `async1 start`、`async2`,然后注册第一个微任务(async1 的后半段);接着 Promise 构造函数同步输出 `promise1`,注册第二个微任务;`script end` 同步输出。微任务队列顺序是「async1 后半段 → promise2」,所以 `async1 end` 在 `promise2` 前面;最后才是 `setTimeout`。

注意:微任务队列严格按照注册顺序执行,不是按代码「看起来重要不重要」。`await` 先注册,就先进先出。

小结

  • 事件循环规则:同步代码 → 清空微任务 → 一个宏任务 → 再清空微任务,循环往复。
  • 微任务:`Promise.then`、`queueMicrotask`、`await` 之后的代码、`MutationObserver`。
  • 宏任务:`script` 本身、`setTimeout`、`setInterval`、I/O、UI 事件。
  • `Promise` 构造函数内同步,`.then` 回调异步。
  • 微任务不释放主线程,密集微任务会阻塞渲染。
  • 分析题先按「同步 / 微 / 宏」三行分类打标,再按注册顺序拼答案,比硬背输出靠谱得多。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-560.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~