事件循环先执行同步代码,再清空微任务队列,最后执行下一个宏任务;setTimeout(0)进入下一轮宏任务,Promise.then等微任务在当前宏任务结束后立即执行。

JavaScript 的事件循环(Event Loop)是理解异步代码执行顺序的核心。面对包含 setTimeout、Promise、async/await、微任务(microtask)和宏任务(macrotask)的复杂代码,输出顺序看似混乱,实则严格遵循事件循环规则。关键在于分清任务类型、执行时机与队列优先级。
宏任务 vs 微任务:执行顺序的底层规则
事件循环每次只从宏任务队列中取出一个任务执行(如:script整体、setTimeout、setInterval、I/O、UI渲染);而每执行完一个宏任务,就会**清空当前所有微任务队列**(如:Promise.then/catch/finally、queueMicrotask、MutationObserver回调)。
- 宏任务之间互相等待,中间插入全部微任务
- 微任务不排队进下一轮宏任务,而是“插队”在当前宏任务结束后立即执行
-
async/await本质是语法糖,await后面的表达式结果决定是否进入微任务:若为非 Promise 值,后续代码同步执行;若为 Promise,则await后的代码被包装进Promise.then→ 归为微任务
分析复杂代码的四步法
遇到多层嵌套的异步代码,按以下步骤拆解更可靠:
-
标记所有任务类型:把每个异步操作明确归类为宏任务(如
setTimeout(fn, 0))或微任务(如Promise.resolve().then(fn)) -
画出执行栈初始路径:从同步代码开始,记录调用顺序,注意
async函数本身是同步入栈、返回 Promise,await处暂停并让出控制权 - 按轮次模拟事件循环:第1轮(初始 script)、第2轮(第一个 setTimeout 回调)、第3轮……每轮后立即执行所有积压微任务
-
注意隐式微任务:例如
Promise.resolve().then()是微任务;new Promise(r => r()).then()中的r()是同步执行,但.then()回调仍进微任务队列
经典陷阱:setTimeout(0) 不等于“立刻执行”
setTimeout(fn, 0) 只是将 fn 推入**下一个宏任务队列**,它必须等当前宏任务 + 所有微任务执行完毕后,才可能被执行。因此它永远排在所有当前微任务之后。
立即学习“Java免费学习笔记(深入)”;
- 即使设为 0,也至少延迟一次事件循环周期
- 多个
setTimeout按注册顺序排队,但可能被中间插入的微任务大幅推迟输出 - 浏览器还可能对高频率
setTimeout进行节流(如最小间隔 4ms),Node.js 则无此限制
实战示例:逐行标注执行时机
看这段常被用来考察事件循环的代码:
console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4);
执行顺序是 1 → 4 → 3 → 2。原因:
- 同步部分:
console.log(1)和console.log(4)立即执行 - 微任务:
Promise.then进入微任务队列,当前宏任务结束立即执行 → 输出 3 - 宏任务:
setTimeout回调进入下一轮宏任务队列 → 输出 2
再加一层 async/await:
async function foo() {
console.log('a');
await Promise.resolve();
console.log('b');
}
foo();
console.log('c');
输出为 a → c → b。因为 await Promise.resolve() 触发微任务调度,console.log('b') 被挂起,等当前宏任务(含 console.log('c'))和所有微任务执行完才运行。


















