事件循环严格遵循“每个宏任务执行完后立即同步清空当前所有微任务队列”的规则,微任务不跨轮次保留,按fifo顺序集中执行。

宏任务嵌套微任务时,事件循环不会“跳过”或“合并”微任务,而是严格遵循:每个宏任务执行完后,**立即、同步地清空当前所有可执行的微任务队列**,再进入下一个宏任务。关键在于:微任务不是“随宏任务一起执行”,而是在宏任务退出后、下一轮宏任务开始前,被集中、连续、无中断地执行。
宏任务与微任务的基本层级关系
JavaScript 的事件循环中,任务分为两个明确优先级层级:
-
宏任务(Macrotask):包括
setTimeout、setInterval、I/O、UI 渲染、script全局代码等;每次只取一个执行。 -
微任务(Microtask):包括
Promise.then/catch/finally、queueMicrotask()、MutationObserver回调等;每次宏任务结束后,会持续从微任务队列头部取出任务执行,直到队列为空。
注意:微任务队列是**单个先进先出(FIFO)队列**,不是按 Promise 创建顺序,而是按进入队列的顺序执行;且它在每次宏任务结束时被完全清空,不跨轮次保留。
嵌套场景下的真实执行顺序示例
看这段典型代码:
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => console.log(3));
}, 0);
Promise.resolve().then(() => console.log(4));
setTimeout(() => {
console.log(5);
}, 0);
console.log(6);
执行过程逐轮分解如下:
-
第一轮宏任务(全局 script):执行
console.log(1)→setTimeout(..., 0)(注册宏任务 A)→Promise.resolve().then(...)(入微任务队列,记为 M1)→setTimeout(..., 0)(注册宏任务 B)→console.log(6)。输出:1、6。 -
立即清空微任务队列:M1 执行 → 输出
4。此时微任务队列为空。 -
第二轮宏任务(宏任务 A):从宏任务队列取出最早注册的 setTimeout 回调 → 执行
console.log(2)→ 再触发Promise.resolve().then(...)(入微任务队列,记为 M2)。输出:2。 -
立即清空微任务队列:M2 执行 → 输出
3。 -
第三轮宏任务(宏任务 B):执行
console.log(5)。输出:5。
最终输出顺序为:1 → 6 → 4 → 2 → 3 → 5。可见:M2 虽由宏任务 A 中产生,但它不会“插队”到 M1 后面——它只是加入当前(第二轮)微任务队列末尾,并在宏任务 A 结束后被立即执行;它也不会推迟到第三轮宏任务之后。
常见误区澄清
-
“微任务能打断宏任务”是错的:微任务总在宏任务函数执行完毕、调用栈完全清空后才开始运行。即使宏任务里写了 100 个
queueMicrotask,它们也都在该宏任务 return 后批量执行,中间不会穿插任何其他宏任务逻辑。 - “嵌套的 Promise.then 会延迟到下一轮事件循环”是错的:只要它出现在某个宏任务执行路径中,其回调就属于本轮微任务,和外层宏任务是否“嵌套”无关;决定时机的是“何时入队”,而非“在哪定义”。
-
多个
queueMicrotask或Promise.then连续调用,会按顺序入队、顺序执行,不会因异步写法产生竞态或重排序(除非跨宏任务)。
调试与验证技巧
想确认某段逻辑是否属于微任务,可用以下方式验证:
- 用
queueMicrotask(() => console.log('micro'))和setTimeout(() => console.log('macro'), 0)对比输出顺序;前者一定早于后者。 - 在 Chrome DevTools 中启用 Async Stack Trace,查看 Promise 回调的调用栈底部是否标记为
microtask。 - 避免依赖“执行时机”的业务逻辑;如需强顺序,显式使用
await Promise.resolve()强制让出到微任务末尾,或改用setTimeout(fn, 0)明确推到下一轮宏任务。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











