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() 显式加入的回调await 后续代码(本质是 Promise 微任务链的一部分)注意:Promise 构造函数内的执行器(executor)是同步运行的,只有其内部的 /
resolvereject 调用才会触发后续微任务。
宏任务每次只执行一个,执行完后检查并清空全部微任务,再取下一个宏任务。典型来源有:
setTimeout / setInterval
setImmediate(Node.js 独有,非标准)fs.readFile 在 Node.js 中)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 函数返回的是 Promise,await 后面的表达式一旦 resolve/reject,后续代码会被包装进微任务。这意味着:
try/catch 包裹的 await 错误,不会同步抛出,而是变成 rejected Promise,需靠 .catch 或顶层 unhandledrejection 捕获await 连续写,它们之间是微任务衔接,不是同步栈;调试时断点可能“跳过”中间状态await Promise.resolve() 会触发一次微任务调度,哪怕值已确定——这在性能敏感循环中可能成为隐性开销真正难调试的,往往不是“谁先谁后”,而是微任务队列被意外延长(比如某个 then 里又创建了新 Promise 链),导致预期中的 DOM 更新延迟或状态不同步。