javascript事件循环是什么_如何理解它的执行机制

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

JavaScript 事件循环不是“轮询”或“定时检查”,而是一个严格遵循「宏任务 → 清空微任务 → 渲染(浏览器)→ 下一个宏任务」顺序的调度机制。它决定了 setTimeoutPromiseasync/await 这些异步代码到底什么时候真正执行。

为什么 Promise.then 总比 setTimeout 先输出?

因为它们分属不同队列:前者进微任务队列,后者进宏任务队列。事件循环每次只取一个宏任务执行,但执行完后**必须立刻清空全部微任务**,不等下一轮。

  • 宏任务(macrotask)包括:script(整体脚本)、setTimeoutsetIntervalI/OUI 渲染(浏览器中)
  • 微任务(microtask)包括:Promise.then/catch/finallyqueueMicrotaskasync/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');

输出顺序是:startfoo startendfoo end。注意:foo end 不是下一轮宏任务,而是本轮宏任务(foo)执行到 await 后暂停,等 Promise resolve 后,把 console.log('foo end') 推入微任务队列,紧接在 end 后执行。

容易踩的坑:零延迟 setTimeout(..., 0) 并不“立刻”执行

它只是把回调放进宏任务队列末尾,要等当前所有同步代码 + 所有微任务跑完,再等到下一轮事件循环才可能执行 —— 所以它永远排在所有微任务之后。

  • 常见误判:以为 setTimeout(() => ..., 0) 能“让出线程”给 UI 更新,其实它比 queueMicrotask 还慢
  • 正确替代:需要“尽快但不打断当前同步流”,优先用 queueMicrotask;需要“下一帧渲染后执行”,用 requestAnimationFramesetTimeout(配合 performance.now() 观察实际延迟)
  • 浏览器中,宏任务之间还可能插入 UI 渲染(如样式计算、布局、绘制),所以 setTimeout 实际延迟往往 ≥ 4ms(受浏览器节流限制)

最常被忽略的一点:事件循环不是“JS 引擎自己决定怎么调度”,而是由宿主环境(浏览器或 Node.js)定义任务队列的类型和优先级。比如 Node.js 有 process.nextTick(比微任务还早),而浏览器没有;MutationObserver 回调属于微任务,但它的触发时机依赖 DOM 变更检测,不是纯 JS 控制。理解这一点,才能真正看懂跨环境行为差异。