DrillLab
第 39 / 105 道39 / 105 · #305

事件循环是怎么工作的

What does the event loop

先自己答,再往下看Answer it yourself first

一句话:JS只有一个主线程,事件循环负责在「调用栈空了」的时候,从任务队列里取下一个任务放上去执行。

完整的一轮(这段是标准答案):

  1. 执行完当前的同步代码(调用栈清空)。
  2. 把微任务队列全部清空—— 注意是「全部」,而且清微任务时新产生的微任务也在这一轮里执行完。
  3. (浏览器)需要的话渲染一帧。
  4. 取一个宏任务执行,回到第 2 步。

谁是微任务:Promise.then/catch/finally、await 之后的代码、queueMicrotask、MutationObserver。
谁是宏任务:setTimeout / setInterval、DOM 事件回调、网络回调、requestAnimationFrame(严格说它在渲染前,单独一档)。

一句话记住优先级:Promise 一定比setTimeout 先跑,即使 setTimeout(…, 0)。

会追问:「异步是谁做的?」—— 不是引擎,是宿主环境(浏览器的 Web API / Node 的 libuv)。引擎只管执行 JS。这条和 #276 是一组。
「setTimeout(fn, 0)真的 0 毫秒吗?」—— 不是「立刻」:它要等当前同步代码和全部微任务跑完、主线程空出来才轮到;另外定时器嵌套超过 5 层后,浏览器会把延迟钳到至少 4ms。所以它只是「尽快,但不是现在」。

Node 的差别(问到就是加分):Node 的宏任务分了六个阶段(timers / pending / poll / check / close…),setImmediate 在 check 阶段;CommonJS 里 process.nextTick 队列先于 Promise 微任务,ES 模块的顶层代码里则是 Promise 先执行。

In one line: JS has one main thread, and the event loop’s job is to pull the next task off a queue and put it on the stack whenever the call stack goes empty.

One full turn — this part is the model answer:

  1. Run the synchronous code to the end (the call stack empties).
  2. Drain the microtask queue completely — note “completely”: microtasks queued while draining also run inside this same turn.
  3. (In a browser) paint a frame if one is needed.
  4. Take one macrotask, run it, go back to step 2.

Microtasks: Promise.then/catch/finally, the code after an await, queueMicrotask, MutationObserver.
Macrotasks: setTimeout / setInterval, DOM event handlers, network callbacks, requestAnimationFrame (strictly it runs just before paint, in a class of its own).

The priority in one line: a Promise always runs before a setTimeout, even setTimeout(…, 0).

Follow-up: “Who actually does the async work?” — not the engine, the host environment (the browser’s Web APIs, libuv in Node). The engine only runs JS. This one pairs with #276.
“Is setTimeout(fn, 0) really zero milliseconds?” — not “right now”: it waits until the synchronous code and every microtask have finished and the main thread is free; and once timers are nested more than five levels deep, browsers clamp the delay to at least 4ms. So it means “as soon as possible, but not now”.

How Node differs (a bonus point if it comes up): Node splits macrotasks into six phases (timers / pending / poll / check / close…), setImmediate lands in the check phase, and in CommonJS the process.nextTick queue runs before the Promise microtasks, while in the top-level code of an ES module the Promises run first.

JavaScript必须能推出来的那道题The question you have to be able to work out示意Illustrative
1console.log("1 同步");
2
3setTimeout(() => console.log("2 宏任务"), 0);
4
5Promise.resolve().then(() => console.log("3 微任务"));
6
7(async () => {
8 console.log("4 同步(await 之前是同步的)");
9 await null;
10 console.log("5 微任务(await 之后)");
11})();
12
13console.log("6 同步");
14
15// 输出:1 同步 -> 4 同步 -> 6 同步 -> 3 微任务 -> 5 微任务 -> 2 宏任务
16//
17// 关键两点:
18// · async 函数体在遇到第一个 await 之前是同步执行的
19// · await 之后的代码等价于 .then 里的代码,是微任务
1console.log("1 sync");
2
3setTimeout(() => console.log("2 macrotask"), 0);
4
5Promise.resolve().then(() => console.log("3 microtask"));
6
7(async () => {
8 console.log("4 sync (everything before await is sync)");
9 await null;
10 console.log("5 microtask (after await)");
11})();
12
13console.log("6 sync");
14
15// Output: 1 sync -> 4 sync -> 6 sync -> 3 microtask -> 5 microtask -> 2 macrotask
16//
17// The two key points:
18// · the body of an async function runs synchronously until the first await
19// · the code after await is the same as code inside .then, so it is a microtask
面试给的题基本是这个变体。抓住两条:同步先跑完;微任务在宏任务前,且一次清空。The question you get in an interview is almost always a variant of this one. Hold on to two rules: all synchronous code runs first; microtasks run before macrotasks, and the whole microtask queue is drained at once.