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

事件循环是怎么工作的

What does the event loop

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

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

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

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

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

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

会追问:「异步是谁做的?」—— 不是引擎, 是宿主环境(浏览器的 Web API / Node 的 libuv)。引擎只管执行 JS。 这条和 #276 是一组。
setTimeout(fn, 0)真的 0 毫秒吗?」—— 不是,浏览器最小约 4ms, 而且要等主线程空闲。所以它只是「尽快,但不是现在」。

Node 的差别(问到就是加分): Node 的宏任务分了六个阶段 (timers / pending / poll / check / close…),setImmediate 在 check 阶段,process.nextTick比所有微任务都优先

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?” — no. Browsers clamp it to roughly 4ms, and it still waits for a free main thread. 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 process.nextTick jumps ahead of every microtask.

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.