JavaScript执行是单线程的,靠事件循环调度微任务(如Promise.then、queueMicrotask)和宏任务(如setTimeout、I/O),微任务在同步代码后立即清空执行,宏任务每次只执行一个且中间插入微任务检查。

JavaScript 的执行是单线程的,但靠任务队列机制实现异步行为。微任务(microtask)和宏任务(macrotask)不是语法层面的概念,而是事件循环(Event Loop)中对任务分类调度的实际规则——理解它们的关键,在于知道哪些操作会进入哪类队列、谁先执行、以及为什么 Promise.then 总比 setTimeout 先触发。
哪些操作产生微任务?
微任务在当前同步代码执行完后、下一轮宏任务开始前立即执行,且会清空整个微任务队列(即连续执行,不穿插渲染或 I/O)。常见来源包括:
-
Promise.prototype.then/.catch/.finally回调(即使Promise.resolve().then(...)) -
queueMicrotask()显式加入的回调 - MutationObserver 的回调
-
await后续代码(本质是 Promise 微任务链的一部分)
注意:Promise 构造函数内的执行器(executor)是同步运行的,只有其内部的 resolve/reject 调用才会触发后续微任务。
哪些操作产生宏任务?
宏任务每次只执行一个,执行完后检查并清空全部微任务,再取下一个宏任务。典型来源有:
立即学习“Java免费学习笔记(深入)”;
-
setTimeout/setInterval -
setImmediate(Node.js 独有,非标准) - I/O 回调(如
fs.readFile在 Node.js 中) - UI 渲染(浏览器中,虽不可直接调度,但属于宏任务周期的一部分)
-
postMessage和MessageChannel的消息处理
特别注意:setTimeout(fn, 0) 并不意味着“立刻执行”,它只是尽快把 fn 推入宏任务队列——实际执行要等当前宏任务 + 所有微任务完成之后。
执行顺序如何确定?看一个典型例子
下面这段代码能清晰体现两者的嵌套关系:
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
Promise.resolve().then(() => {
console.log(4);
setTimeout(() => console.log(5), 0);
});
console.log(6);
输出顺序是:1 → 6 → 3 → 4 → 2 → 5。原因如下:
- 同步代码先执行:
1和6 - 然后清空微任务队列:两个
Promise.then(3和4),其中第二个还触发了一个新的setTimeout(推入宏任务队列) - 接着取下一个宏任务:
setTimeout的2 - 最后执行新推入的宏任务:
5
关键点:微任务可以递归产生新微任务(比如 then 里再返回 Promise),它们都会被追加到当前微任务队列末尾,并在本轮全部执行完;而宏任务之间永远是串行的,中间必然插入微任务检查。
容易忽略的坑:async/await 与错误捕获
async 函数返回的是 Promise,await 后面的表达式一旦 resolve/reject,后续代码会被包装进微任务。这意味着:
- 未被
try/catch包裹的await错误,不会同步抛出,而是变成 rejected Promise,需靠.catch或顶层unhandledrejection捕获 - 多个
await连续写,它们之间是微任务衔接,不是同步栈;调试时断点可能“跳过”中间状态 -
await Promise.resolve()会触发一次微任务调度,哪怕值已确定——这在性能敏感循环中可能成为隐性开销
真正难调试的,往往不是“谁先谁后”,而是微任务队列被意外延长(比如某个 then 里又创建了新 Promise 链),导致预期中的 DOM 更新延迟或状态不同步。










