JavaScript 事件循环并非等到所有异步操作完成后才处理,而是按照严格的节奏分步推进:先执行当前同步代码,再清空所有微任务,然后执行一个宏任务,如此循环往复。其核心在于微任务(如 Promise.then)的优先级始终高于宏任务(如 setTimeout),即使 setTimeout(..., 0) 写在前面,微任务也一定会先执行。

宏任务和微任务如何区分
宏任务可以理解为“大块工作”,例如整个 script 脚本、setTimeout/setInterval 回调、I/O 完成通知、UI 渲染、DOM 事件处理等。它们被排入宏任务队列,每次只取出一个执行。
微任务则是“紧急插队的小任务”,例如 Promise.then/catch/finally、queueMicrotask()、MutationObserver 回调。一旦同步代码执行完毕,引擎会立即回头将当前所有微任务全部执行完,一个不留。
关键区别在于优先级:微任务永远比宏任务先执行。即使 setTimeout(…, 0) 写在 Promise.then 前面,输出顺序依然是 Promise 先、setTimeout 后。
事件循环每一轮的具体执行顺序
- 执行当前宏任务中的全部同步代码(如 console.log、函数调用、变量赋值等)
- 遇到 setTimeout 则放入宏任务队列;遇到 Promise.then 则放入微任务队列
- 同步代码执行完毕,调用栈清空后,立即检查微任务队列
- 依次执行所有已排队的微任务(包括执行过程中新产生的微任务)
- 微任务队列清空后,浏览器可能触发一次 UI 渲染(例如重绘)
- 从宏任务队列头部取出下一个任务,开始新一轮循环
async/await 如何参与事件循环调度
async 函数本身作为宏任务入口,但 await 后面的表达式如果返回 Promise,则会将后续代码包装成微任务。
例如:
async function fn() {
console.log('1');
await Promise.resolve();
console.log('2');
}
fn();
console.log('3');
输出顺序为 1 → 3 → 2。这是因为 await 将 console.log('2') 推进了微任务队列,而 console.log('3') 是同步代码,紧随 fn() 调用后执行。
事件循环常见误区与注意事项
- setTimeout(..., 0) 并不代表“立刻执行”,而是尽快安排进宏任务队列,至少要等当前同步代码和所有微任务执行完毕
- Promise.then 不是“马上运行”,它注册的是微任务,必须等到本轮同步代码执行结束才触发
- fetch() 返回 Promise,但只有在 .then 或 await 时才会决定回调何时加入微任务队列,并非请求发出那一刻就排队
- 多个 Promise.then 链式调用时,每个 .then 都会生成新微任务,并按照注册顺序依次执行
