Skip to main content

事件循环

结论先行(一句话版)

浏览器是: 👉 执行 一个 macrotask 👉 然后 清空 microtasks 👉 然后 “可能” 进行一次渲染 👉 再进入下一轮循环

注意两个关键限定词:

  • 一个 macrotask
  • 渲染是 可能,不是必然

一、标准事件循环的一次“完整迭代”

你可以在脑中记成这段伪代码:

while (true) {
task = getNextMacrotask();
execute(task);

while (microtaskQueue.notEmpty()) {
execute(microtask);
}

if (shouldRender()) {
render();
}
}

你刚才问的那句话,翻译成底层就是:

“是不是一个 macrotask → 清空 microtask → 再 render?”

👉 是的,顺序就是这样


二、逐步拆解(为什么是这个顺序)

1️⃣ 为什么是「一个」macrotask?

如果不是一个,而是:

while (macrotaskQueue.notEmpty()) {
execute(macrotask);
}

那会导致:

  • 定时器 / 网络 / IO 把 JS 线程占满
  • 用户输入永远插不进来
  • 页面卡死

👉 所以浏览器强制:一次只跑一个


2️⃣ 为什么 microtask 必须“清空”?

microtask 的设计目标是:

“在同一个逻辑连续体里执行”

典型例子:

doA();
Promise.then(doB);
doC();

开发者预期顺序:

A → C → B

如果 microtask 不清空:

  • 状态可能被半更新
  • 抽象直接崩溃

👉 所以规范规定:

microtask queue 要一直跑到空


3️⃣ 为什么 render 在 microtask 之后?

因为 render 依赖的是:

  • 最终稳定的 DOM
  • 最终稳定的 state

如果 DOM 还会被 Promise 改:

Promise.then(() => (el.style.width = "100px"));

那先 render 再改,屏幕就会抖。

👉 所以必须:

JS(含 microtask)完全跑完

才有 render 的资格

三、关键细节:render 不是每一轮都有

这是很多人忽略、也是你这一步已经踩到的地方。

浏览器会问一句:

❓ 这一轮值得 render 吗?

只有在这些情况下,才 render:

  • DOM 有变化
  • CSS 有变化
  • 有 rAF 回调
  • 距离上一次渲染够久了(60Hz)

否则:

macrotask

microtasks

(跳过 render)

👉 这是为什么纯 JS 计算可以“跑很多轮”才看到一帧画面


四、把你关心的几个东西放进这个顺序里

1️⃣ setTimeout

setTimeout(fn, 0);

位置是:

  • fn = macrotask
  • 等下一轮循环

2️⃣ Promise.then

Promise.resolve().then(fn);

位置是:

  • microtask
  • 一定在本轮 macrotask 后执行
  • 一定在 render 前

3️⃣ requestAnimationFrame

requestAnimationFrame(fn);

位置是:

  • 介于 microtask 与 render 之间
  • 在 layout / paint 前

顺序是:

macrotask
→ microtasks
→ rAF
→ layout / paint

五、一个经典验证例子(你可以在脑中跑)

setTimeout(() => console.log("timeout"));

Promise.resolve().then(() => console.log("promise"));

console.log("sync");

执行顺序一定是:

sync
promise
timeout

为什么?

  • sync:当前 macrotask
  • promise:microtask(立刻清空)
  • timeout:下一个 macrotask

六、一个非常重要的进阶点(你已经到这层了)

microtask 会“饿死渲染”

function loop() {
Promise.resolve().then(loop);
}
loop();

发生什么?

  • macrotask 执行
  • microtask 永远不为空
  • render 永远进不去

👉 页面彻底假死

这也是为什么:

  • React
  • 浏览器
  • Scheduler

几乎不用 Promise 做调度


七、工程师级最终总结(可以直接当心智模型)

**一次 event loop ≈ 一个 macrotask

  • 清空 microtasks +(如果需要)一帧渲染**