JavaScript事件循环严格遵循“宏任务→清空微任务→渲染→下一宏任务”顺序;Promise.then属微任务,setTimeout属宏任务,故前者总先执行。

JavaScript 事件循环不是“轮询”或“定时检查”,而是一个严格遵循「宏任务 → 清空微任务 → 渲染(浏览器)→ 下一个宏任务」顺序的调度机制。它决定了 setTimeout、Promise、async/await 这些异步代码到底什么时候真正执行。
为什么 Promise.then 总比 setTimeout 先输出?
因为它们分属不同队列:前者进微任务队列,后者进宏任务队列。事件循环每次只取一个宏任务执行,但执行完后**必须立刻清空全部微任务**,不等下一轮。
- 宏任务(macrotask)包括:
script(整体脚本)、setTimeout、setInterval、I/O、UI 渲染(浏览器中) - 微任务(microtask)包括:
Promise.then/catch/finally、queueMicrotask、async/await的 await 后续部分、MutationObserver - 关键规则:每执行完一个宏任务,就同步执行所有当前微任务,哪怕中间又产生了新微任务(比如在
then里再调一次Promise.resolve().then),也会被追加并本轮清空
async/await 在事件循环里怎么算任务?
async 函数本身是宏任务(进入调用栈即开始执行),但 await 后面的表达式一旦返回 Promise,其后续逻辑就变成微任务 —— 和直接写 .then 等价。
console.log('start');
async function foo() {
console.log('foo start');
await Promise.resolve();
console.log('foo end');
}
foo();
console.log('end');
输出顺序是:start → foo start → end → foo end。注意:foo end 不是下一轮宏任务,而是本轮宏任务(foo)执行到 await 后暂停,等 Promise resolve 后,把 console.log('foo end') 推入微任务队列,紧接在 end 后执行。
立即学习“Java免费学习笔记(深入)”;
容易踩的坑:零延迟 setTimeout(..., 0) 并不“立刻”执行
它只是把回调放进宏任务队列末尾,要等当前所有同步代码 + 所有微任务跑完,再等到下一轮事件循环才可能执行 —— 所以它永远排在所有微任务之后。
- 常见误判:以为
setTimeout(() => ..., 0)能“让出线程”给 UI 更新,其实它比queueMicrotask还慢 - 正确替代:需要“尽快但不打断当前同步流”,优先用
queueMicrotask;需要“下一帧渲染后执行”,用requestAnimationFrame或setTimeout(配合performance.now()观察实际延迟) - 浏览器中,宏任务之间还可能插入 UI 渲染(如样式计算、布局、绘制),所以
setTimeout实际延迟往往 ≥ 4ms(受浏览器节流限制)
最常被忽略的一点:事件循环不是“JS 引擎自己决定怎么调度”,而是由宿主环境(浏览器或 Node.js)定义任务队列的类型和优先级。比如 Node.js 有 process.nextTick(比微任务还早),而浏览器没有;MutationObserver 回调属于微任务,但它的触发时机依赖 DOM 变更检测,不是纯 JS 控制。理解这一点,才能真正看懂跨环境行为差异。











