事件循环(宏任务 vs 微任务)
阶段 1.2|事件循环(宏任务 vs 微任务)
一、四个组成部分
┌───────────────┐
│ 调用栈 Call Stack │ ← 同步代码在这里执行(LIFO)
└───────────────┘
↓ 遇到异步
┌───────────────┐
│ 宿主环境 Web APIs │ ← 浏览器/Node:定时器、网络、IO(有别的线程)
└───────────────┘
↓ 完成后把回调放进队列
┌───────────────┐
│ 微任务队列 Microtask │ ← Promise.then / queueMicrotask / MutationObserver
│ 宏任务队列 Macrotask │ ← setTimeout / 事件 / I/O / script 整体
└───────────────┘
↓
┌───────────────┐
│ 事件循环 Event Loop │ ← 栈空时,按规则搬运队列里的任务到栈上执行
└───────────────┘
二、执行规则(背下来)⭐
- 先执行同步代码(栈内)。
- 栈空后,清空整个微任务队列(执行过程中新产生的微任务也要一并清空)。
- 取一个宏任务执行。
- 回到第 2 步,循环。
口诀:同 → 微(全清)→ 宏(取一)→ 微(全清)→ 宏(取一)……
三、谁是宏任务,谁是微任务
| 宏任务(Macrotask) | 微任务(Microtask) |
|---|---|
<script> 整体代码 |
Promise.then / catch / finally |
setTimeout / setInterval |
await 之后的代码 |
| DOM 事件(click 等) | queueMicrotask() |
| I / O(Node) | MutationObserver |
postMessage / MessageChannel |
process.nextTick(Node,优先级最高) |
重要:
await xxx后面的代码,等价于放进Promise.then→ 微任务。
注意:new Promise(fn)里的fn是同步执行的,只有.then回调是微任务。
四、由浅入深的例子
例 1:基础(本节自检原型)
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
输出:1 → 4 → 3 → 2
解析:同步 1、4;微任务 3;宏任务 2。
例 2:await 就是微任务
console.log('A')
async function f() {
console.log('B')
await null // 或 await Promise.resolve()
console.log('C') // ← 这里被放进微任务
}
f()
console.log('D')
输出:A → B → D → C
await 前的 B 同步执行;await 后面的 C 让出,等同步代码跑完才执行。
例 3:微任务全清(含嵌套产生)
console.log('start')
setTimeout(() => console.log('timeout'), 0)
Promise.resolve()
.then(() => {
console.log('micro1')
Promise.resolve().then(() => console.log('micro-nested'))
})
.then(() => console.log('micro2'))
console.log('end')
输出:start → end → micro1 → micro-nested → micro2 → timeout
关键点:微任务执行中产生的新微任务,也在本轮一起清完,然后才轮到 timeout。
例 4:宏任务里再产生微任务
setTimeout(() => {
console.log('macro1')
Promise.resolve().then(() => console.log('micro-in-macro'))
}, 0)
setTimeout(() => console.log('macro2'), 0)
输出:macro1 → micro-in-macro → macro2
每执行一个宏任务,就立刻清一次微任务,再执行下一个宏任务。
例 5:综合题(进阶自检)
console.log('1')
setTimeout(() => console.log('2'), 0)
async function a() {
console.log('3')
await b()
console.log('4')
}
async function b() { console.log('5') }
a()
new Promise(r => { console.log('6'); r() }).then(() => console.log('7'))
console.log('8')
输出:1 → 3 → 5 → 6 → 8 → 4 → 7 → 2
解析:
- 同步:
1、3(进 a)、5(进 b)、6(Promise 构造器同步)、8 - 微任务队列:
a中await b()后的4(先注册)、.then的7 - 宏任务:
2
五、浏览器 vs Node 的差异
- 浏览器:每执行一个宏任务 → 清微任务 → (可能渲染)→ 下一个宏任务。
- Node:
process.nextTick队列 先于 Promise 微任务队列清空。- Node 11+ 起,行为与浏览器对齐(每个宏任务后清空微任务)。
- 渲染时机:微任务清空后、下一个宏任务之前,浏览器可能插入渲染(
requestAnimationFrame在渲染前执行)。
六、两个真实坑
坑 1:微任务死循环 → 页面卡死
function loop() { queueMicrotask(loop) } // ❌ 微任务永远清不完
loop()
// 后果:永远进不了渲染和下一个宏任务,页面彻底假死(比 setTimeout 死循环更狠)
坑 2:setTimeout(fn, 0) 不是 0ms
- 最快也要等当前同步 + 微任务跑完;
- HTML 规范:嵌套超过 5 层的定时器,最小间隔 4ms。
✅ 自检(答案)
题:写出下面输出顺序
console.log('A')
setTimeout(() => console.log('B'), 0)
Promise.resolve().then(() => console.log('C'))
console.log('D')
答:A → D → C → B(同步 A、D → 微任务 C → 宏任务 B)
追问 1:微任务队列没清完,会去执行下一个宏任务吗?
答:不会。必须把微任务(含执行中新增的)全部清空,才取下一个宏任务。
追问 2:await 后面的代码属于什么任务?
答:微任务。
追问 3:异步等于多线程吗(复习 1.1)?
答:不等。回调最终仍在主线程串行执行,只是"延后",不是"同时"。

浙公网安备 33010602011771号