事件循环(宏任务 vs 微任务)

阶段 1.2|事件循环(宏任务 vs 微任务)

一、四个组成部分

┌───────────────┐
│   调用栈 Call Stack  │ ← 同步代码在这里执行(LIFO)
└───────────────┘
        ↓ 遇到异步
┌───────────────┐
│  宿主环境 Web APIs    │ ← 浏览器/Node:定时器、网络、IO(有别的线程)
└───────────────┘
        ↓ 完成后把回调放进队列
┌───────────────┐
│ 微任务队列 Microtask │ ← Promise.then / queueMicrotask / MutationObserver
│ 宏任务队列 Macrotask │ ← setTimeout / 事件 / I/O / script 整体
└───────────────┘
        ↓
┌───────────────┐
│  事件循环 Event Loop │ ← 栈空时,按规则搬运队列里的任务到栈上执行
└───────────────┘

二、执行规则(背下来)⭐

  1. 先执行同步代码(栈内)。
  2. 栈空后,清空整个微任务队列(执行过程中新产生的微任务也要一并清空)。
  3. 取一个宏任务执行。
  4. 回到第 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)?
答:不等。回调最终仍在主线程串行执行,只是"延后",不是"同时"。

posted @ 2026-10-13 15:44  寅信  阅读(1)  评论(0)    收藏  举报