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 的微任务分两级:process.nextTick的优先级比 Promise 更高—— 它有自己的队列,在所有 Promise 微任务之前执行

和浏览器的差别(这是考点):浏览器只有「宏任务 / 微任务」两档, 每次只取一个宏任务; Node 分了六个阶段, 同一阶段里的队列会一次取完
另外 Node 多了setImmediateprocess.nextTick这两个浏览器没有的东西。

会追问: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. timerssetTimeout and setInterval callbacks that are due
  2. pending callbacks — system callbacks deferred from the previous turn
  3. idle / prepare — internal use
  4. pollpicks up new I/O events; this is where the process spends most of its time
  5. checksetImmediate callbacks
  6. close callbacks — things like socket.on("close")

The key point: microtasks are drained between every phase. And Node has two levels of them — process.nextTick outranks Promises. It gets its own queue and runs before any Promise microtask.

How this differs from the browser (this is the part they are testing): the browser has just two tiers, macrotask and microtask, and takes one macrotask per turn. Node has six phases, and within a phase it drains the whole queue.
Node also has setImmediate and process.nextTick, neither of which exists in a browser.

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 的执行顺序示意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// 记住:nextTick 比 Promise 更优先,这是 Node 独有的
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// Remember: nextTick runs before Promise, and that part is specific to Node