宏任务和微任务是JavaScript事件循环中两类任务,宏任务(如setTimeout)执行完后必须清空全部微任务队列(如Promise.then)才执行下一个宏任务,这是理解async/await、setTimeout(0)等执行顺序的关键。

什么是宏任务和微任务,为什么必须分清
JavaScript 的事件循环本质是按顺序执行两类任务:宏任务(macro task)和微任务(micro task)。不区分它们,就无法预测 setTimeout、Promise.then、async/await 的实际执行顺序。
常见宏任务包括:setTimeout、setInterval、setImmediate(Node.js)、I/O 回调、UI 渲染;常见微任务包括:Promise.then/catch/finally、queueMicrotask、MutationObserver 回调。
关键规则:每次宏任务执行完后,会**清空全部当前微任务队列**,再取下一个宏任务。这意味着哪怕在 setTimeout 回调里新建一个 Promise,它的 .then 也会比下一个 setTimeout 先运行。
console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4); // 输出:1 → 4 → 3 → 2
async/await 在事件循环中到底做了什么
async/await 是语法糖,底层完全基于 Promise 和微任务调度。每个 await 表达式后面如果不是 Promise,会被自动包装成 Promise.resolve(value),然后挂起函数,把后续代码塞进微任务队列。
立即学习“Java免费学习笔记(深入)”;
-
await后的表达式**同步求值**,但 await 本身不会阻塞线程,只是暂停函数执行上下文 - 函数恢复执行时,是在**下一个微任务阶段**,不是立即、也不是下一轮宏任务
- 多个连续
await不会“累积延迟”,它们之间仍是微任务链,没有宏任务插入
async function f() {
console.log('a');
await Promise.resolve();
console.log('b');
}
f();
console.log('c');
// 输出:a → c → b
为什么 setTimeout(fn, 0) 不是“立刻执行”
setTimeout(fn, 0) 并不等于同步执行,它只是把 fn 推入**宏任务队列尾部**,等待当前所有同步代码 + 所有微任务执行完毕后,才轮到它。浏览器或 Node.js 对最小延迟也有强制下限(通常 ≥ 4ms),所以它实际是“尽快但不早于下一轮宏任务开始”。
容易踩的坑:
- 误以为
setTimeout(fn, 0)能绕过当前调用栈 —— 它不能,仍需等栈清空 - 在 React 或 Vue 的响应式更新中滥用它来“等 DOM 更新”,不如用
queueMicrotask或框架提供的nextTick - 嵌套多层
setTimeout(..., 0)会不断往后推,形成不可控的延迟链
如何用 queueMicrotask 替代 hack 式微任务调度
过去常用 Promise.resolve().then() 来手动触发微任务,但这是副作用写法,语义不清。现代环境(Chrome 69+、Node.js 11+、Firefox 71+)支持原生 queueMicrotask,语义明确、无额外对象开销、且不抛错(Promise 构造器可能因 reject 未捕获而报 unhandledrejection)。
适用场景:
- 需要在当前任务结束后、下次渲染前执行轻量逻辑(如批量更新状态)
- 避免
Promise构造器带来的性能开销(尤其高频调用时) - 替代
process.nextTick在跨平台工具中的使用(注意:Node.js 中nextTick优先级高于queueMicrotask)
queueMicrotask(() => {
console.log('this runs after sync code, before next macro task');
});
真正难的不是记住规则,而是意识到:异步不是“时间问题”,是“任务排队位置问题”。只要看清楚某段代码最终落在哪个队列、什么时候被取出,事件循环就不再黑盒。











