事件循环每次清空调用栈后先执行所有微任务(Promise.then、queueMicrotask等),再执行一个宏任务(setTimeout、requestAnimationFrame等);微任务按入队顺序执行且不可中断,宏任务每次仅取一个。

JavaScript 的事件循环不是“等所有同步代码跑完再执行异步”,而是由调用栈、任务队列(宏任务/微任务)、以及执行上下文共同协作的实时调度机制。理解它关键不在于背流程,而在于知道 setTimeout、Promise.then、queueMicrotask 这些 API 到底往哪塞、什么时候取、谁先谁后。
宏任务和微任务的执行顺序怎么定?
每次调用栈清空后,引擎会:先执行所有已入队的微任务(Promise.then、queueMicrotask、MutationObserver),再取一个宏任务(setTimeout、setInterval、I/O、UI 渲染)执行。这个“一次只取一个宏任务”的规则,是很多嵌套 setTimeout 表现不符合直觉的根本原因。
常见错误现象:以为 setTimeout(() => console.log(1), 0) 一定在 Promise.resolve().then(() => console.log(2)) 后面输出 —— 实际上是 2 先,1 后,因为前者是微任务,后者是宏任务。
- 微任务队列在每次宏任务结束后立即清空,且不会被新的微任务插入打断(即递归调用
queueMicrotask也会等本轮全部跑完) -
await后的代码会被包装成微任务,等价于Promise.then -
requestAnimationFrame是宏任务,但浏览器会在下一次重绘前执行,时机比setTimeout(..., 0)更早(不过仍晚于微任务)
Promise.then 和 queueMicrotask 有啥区别?
两者都进微任务队列,但语义和使用场景不同:Promise.then 绑定的是 Promise 状态变化后的回调,自带错误捕获和链式传递;queueMicrotask 是纯粹的“下一轮微任务调度”,无状态、无异常透传,适合解耦或避免 Promise 构造开销。
立即学习“Java免费学习笔记(深入)”;
性能影响:频繁创建 Promise(比如在循环里写 Promise.resolve().then(...))会产生额外对象分配;queueMicrotask 更轻量,但无法像 Promise 那样自然处理异步错误传播。
queueMicrotask(() => console.log('micro1'));
Promise.resolve().then(() => console.log('promise1'));
queueMicrotask(() => console.log('micro2'));
// 输出顺序一定是:micro1 → promise1 → micro2
// 因为所有微任务按入队顺序执行,不分来源
为什么 setTimeout(..., 0) 不是立刻执行?
因为 setTimeout 的回调被放入宏任务队列,必须等当前调用栈 + 所有微任务全部完成之后,才可能被执行。即使设为 0,它也要排队等“下一轮事件循环”的开始 —— 而不是“下一毫秒”。
容易踩的坑:
- 误以为
setTimeout(fn, 0)能让 DOM 更新“立刻可见”:实际上它仍晚于微任务,也晚于当前帧的渲染时机;真要等渲染后执行,该用requestAnimationFrame或setTimeout套两层 - 在 Node.js 中,
setImmediate和setTimeout(..., 0)所处阶段不同(check 阶段 vs timer 阶段),执行顺序受当前事件循环阶段影响,不可假设一致 - 浏览器对非常小的超时值(如
1ms)会自动节流到 4ms,这是规范要求,不是 bug
真正卡住理解的,往往不是“宏任务微任务谁先”,而是没意识到:事件循环每轮只取一个宏任务,但微任务可以无限嵌套(只要不爆栈),而且 UI 渲染本身就是一个隐式的宏任务节点 —— 它不会插在微任务中间,也不会跳过。这点在做动画、表单校验、或者调试 React 的 useEffect 执行时机时,特别容易翻车。











