宏任务与微任务交互的关键是“执行—检查—再执行”节奏:同步代码先执行,宏任务结束后立即清空全部微任务,再取下一个宏任务;微任务无间隔、全清空,宏任务间必隔微任务周期。

理清宏任务与微任务的交互,关键不是背分类,而是抓住事件循环中“执行—检查—再执行”的节奏。它不靠记忆优先级,而靠理解每个环节谁在什么时候“插队”。
同步代码是起点,不是背景音
所有异步逻辑都从同步代码开始铺开。比如 new Promise(r => { console.log('A'); r(); }) 中的 console.log('A') 是同步执行的,.then() 才进微任务队列。很多人误以为 Promise 整体是异步,其实构造函数体是同步的——这直接影响输出顺序。
- 同步代码跑完,才触发微任务检查
- 哪怕只有一行
console.log,它也属于当前宏任务的一部分 - 宏任务本身(如整个
<script></script>)就是第一个被取出来的任务
微任务不是“一次执行一个”,而是“一次清空全部”
只要当前宏任务结束,引擎立刻轮询微任务队列,把里面所有已排队的任务按顺序执行完,中途不穿插任何宏任务。哪怕你在 then 里又 Promise.resolve().then(...),新产生的微任务也会塞进本轮队列尾部,仍算在“这一次清空”里。
-
Promise.resolve().then(() => console.log(1)).then(() => console.log(2))输出 1、2,中间不会被setTimeout打断 -
queueMicrotask和MutationObserver同样遵循这个“全清”规则 - async/await 的
await后代码,本质就是被包装进微任务,不是“接着往下走”
宏任务之间有天然间隔,微任务没有
每个宏任务执行完后,必须先清空微任务,再取下一个宏任务。这意味着:UI 渲染、setTimeout 回调、用户点击事件,彼此之间至少隔一个“微任务清空周期”。而微任务之间没有这种间隔——它们共享同一轮执行窗口。
-
setTimeout(() => console.log('t1'), 0)和setTimeout(() => console.log('t2'), 0)属于两个不同宏任务,必然分两轮执行 - 但
Promise.then和queueMicrotask混用,只要在同一宏任务后产生,就全在同一次微任务阶段跑完 - 浏览器可能在微任务清空后、下个宏任务前做一次渲染,这是性能优化点,不是 JS 引擎控制的
嵌套和递归会放大执行层级,但不改变规则
async 函数内部 await 多次、Promise 链式调用多层、甚至微任务里再注册微任务,看起来复杂,其实只是不断往队列里加新任务。只要记住:每轮只处理一个宏任务 + 全部当前微任务,就不会被嵌套绕晕。
- 一个
async函数调用本身是同步的,返回 Promise;await 后代码被推入微任务 -
await Promise.all([p1, p2])不改变规则:all 完成后,后续代码仍是微任务 - 微任务里调
setTimeout→ 新宏任务进队尾;调Promise.resolve().then→ 新微任务进本轮队尾











