JavaScript 事件循环彻底搞懂:微任务与宏任务面试必问
学完这篇你能得到:拿到任何一段混着 `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` 里的回调才是微任务。很多人栽在这一点上。
第三步:背下这条调度规则
事件循环每一轮的顺序固定是:
- 执行同步代码,直到调用栈清空;
- 清空整个微任务队列(注意:微任务执行时新产生的微任务,也在本轮一起清掉,直到队列彻底空);
- 浏览器判断是否需要渲染(可能在这一步重绘);
- 从宏任务队列取出一个宏任务执行;
- 回到第 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` 回调异步。
- 微任务不释放主线程,密集微任务会阻塞渲染。
- 分析题先按「同步 / 微 / 宏」三行分类打标,再按注册顺序拼答案,比硬背输出靠谱得多。
转载请注明出处,版权归原作者所有。
见习用户





