DrillLab
第 88 / 105 道88 / 105 · #313

Node.js 的事件循环是怎么工作的

How does the event loop work in Node.js

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

一句话:Node 是单线程执行 JS + 多线程做 I/O。主线程跑 JS,耗时的 I/O 交给 libuv 的线程池或操作系统,完成后把回调放进队列,事件循环再取出来执行。

六个阶段(顺序要记住):

  1. timers—— 到期的 setTimeout /setInterval
  2. pending callbacks—— 上一轮延后的系统回调
  3. idle / prepare —— 内部用
  4. poll——取新的 I/O 事件,大部分时间待在这里
  5. check——setImmediate 的回调
  6. close callbacks——socket.on("close") 这类

关键:自 Node 11 起,每执行完一个回调(不论哪个阶段),都会先清空 nextTick 队列和 Promise 微任务,再执行下一个回调—— 这一点已和浏览器一致(Node 10 及以前才是「整个阶段跑完再清」,旧的面试资料里常见这个说法)。Node 的微任务分两级:process.nextTick有自己的队列,和 Promise 微任务分开排。

和浏览器的差别(这是考点):Node 把宏任务分成六个阶段,多了setImmediate和 process.nextTick这两个浏览器没有的东西;CommonJS 里 nextTick 队列先于 Promise 微任务,ES 模块的顶层代码里则是 Promise 先执行。

会追问:「setTimeout(fn, 0) 和setImmediate 谁先?」——不确定!在主模块里两者顺序取决于进程启动耗时;但在 I/O 回调里,setImmediate 一定更早(因为 check 阶段紧跟在 poll 后面,而 timers 要等下一轮)。能答出「主模块里不确定」很加分。
「CPU 密集任务怎么办?」—— 单线程会被长时间占住,其他请求都得等。用 worker_threads、子进程,或者干脆交给别的服务。

In one line: Node runs your JavaScript on one thread and its I/O on many. The main thread runs JS; anything slow goes to libuv’s thread pool or straight to the OS, and when it finishes the callback is queued for the event loop to pick up.

Six phases, and the order matters:

  1. timers — setTimeout and setInterval callbacks that are due
  2. pending callbacks — system callbacks deferred from the previous turn
  3. idle / prepare — internal use
  4. poll — picks up new I/O events; this is where the process spends most of its time
  5. check — setImmediate callbacks
  6. close callbacks — things like socket.on("close")

The key point: since Node 11, after every single callback (in any phase) Node drains the nextTick queue and the Promise microtasks before running the next callback — the same as browsers. (Only Node 10 and earlier drained them once per phase, which is what many older interview notes still say.) Node has two levels of microtask: process.nextTick has its own queue, separate from the Promise microtasks.

How this differs from the browser (this is the part they are testing): Node splits macrotasks into six phases and adds setImmediate and process.nextTick, neither of which exists in a browser; in CommonJS the nextTick queue runs before Promise microtasks, while in the top-level code of an ES module the Promises run first.

Follow-up: “Which fires first, setTimeout(fn, 0) or setImmediate?” — it is not guaranteed. At the top level of the main module the order depends on how long startup took. But inside an I/O callback setImmediate always wins, because check comes right after poll while timers has to wait for the next turn. Saying “undefined in the main module” is the answer that earns you points.
“What about CPU-bound work?” — one thread means it blocks everything. Use worker_threads, a child process, or hand the job to a different service entirely.

JavaScriptNode 的执行顺序The order Node runs things in示意Illustrative
1console.log("1 同步");
2
3setTimeout(() => console.log("2 timers"), 0);
4setImmediate(() => console.log("3 check"));
5Promise.resolve().then(() => console.log("4 promise 微任务"));
6process.nextTick(() => console.log("5 nextTick"));
7
8console.log("6 同步");
9
10// 输出:1 同步 -> 6 同步 -> 5 nextTick -> 4 promise 微任务
11// -> 2 timers 和 3 check(这两个的相对顺序在主模块里不保证)
12//
13// 这是存成 .cjs(CommonJS)运行的结果;存成 .mjs 运行时,4 promise 会排在 5 nextTick 前面
1console.log("1 sync");
2
3setTimeout(() => console.log("2 timers"), 0);
4setImmediate(() => console.log("3 check"));
5Promise.resolve().then(() => console.log("4 promise microtask"));
6process.nextTick(() => console.log("5 nextTick"));
7
8console.log("6 sync");
9
10// Output: 1 sync -> 6 sync -> 5 nextTick -> 4 promise microtask
11// -> 2 timers and 3 check (their relative order is not guaranteed in the main module)
12//
13// This is the output saved as .cjs (CommonJS); saved as .mjs, 4 promise comes before 5 nextTick