跳到主要内容

Node 事件循环机制

Node.js 通过事件循环机制来运行 JavaScript 代码,同时提供线程池处理 I/O 操作任务。下面我们来深入探讨 Node.js 的事件循环机制。

Node.js 中的两种线程

在 Node.js 中,主要有两种线程:

事件循环线程:负责 JavaScript 代码的执行,包括模块加载(require)、同步执行回调函数以及注册新的任务等。

线程池线程:由 C++的libuv库实现,负责处理诸如文件 I/O、网络 I/O 等异步 I/O 操作,以及一些 CPU 密集型任务。

Node.js事件循环示意图

Node.js 的系统架构

Node.js 的系统由事件循环线程和线程池两大部分组成,分别负责任务的调度和执行。

事件循环线程作为整个 Node.js 进程的核心,负责 JavaScript 代码的执行。JavaScript 是单线程的,但通过事件循环机制,可以实现非阻塞式的异步 I/O。

线程池由libuv提供,内部维护了一定数量的线程,用于执行异步 I/O 操作。当事件循环将异步 I/O 任务交给线程池处理后,可以继续执行后面的 JavaScript 代码,从而实现了非阻塞。

Node.js 事件循环的阶段

与浏览器不同,Node.js 的事件循环中只有宏任务队列,没有微任务队列。每一轮事件循环分为如下 6 个阶段:

  1. Timers:执行setTimeout/setInterval的回调函数。
  2. Pending callbacks:执行某些系统操作的回调函数,如 TCP 错误类型的回调。
  3. Idle, prepare:仅供内部使用。
  4. Poll:执行与 I/O 相关的回调,在适当条件下会阻塞。
  5. Check:执行setImmediate的回调函数。
  6. Close callbacks:执行关闭请求的回调函数,如socket.on('close', ...)

其中,Poll阶段是整个事件循环中最重要的阶段。在这个阶段,事件循环会检查是否有新的异步 I/O 事件需要处理,然后将这些事件的回调函数加入Poll队列并执行。

如果Poll队列为空,则事件循环会检查是否有到期的Timer回调,如果有则进入Timers阶段并执行;如果没有,则会在Poll阶段等待,直到有新的 I/O 事件到来或者有Timer回调到期。

宏任务与微任务的执行顺序

虽然 Node.js 中没有微任务队列,但某些特定的 API 提供了类似微任务的功能,比如Promise.thenprocess.nextTick

process.nextTick的回调会在每一个阶段执行完后立即执行,优先级高于Promise。也就是说,执行顺序为:

宏任务 -> process.nextTick -> Promise.then -> 宏任务...

举个例子:

console.log('start');

setTimeout(() => {
console.log('timeout');
}, 0);

Promise.resolve().then(() => {
console.log('promise');
});

process.nextTick(() => {
console.log('nextTick');
});

console.log('end');

输出结果为:

start
end
nextTick
promise
timeout

不同 Node.js 版本的差异

在 Node.js v10 及更早版本中,微任务的清空时机是在事件循环的阶段切换时。

而从 Node.js v11 开始,与浏览器的事件循环机制趋于一致,每执行一个宏任务就会清空微任务队列。

这种差异可能会导致同样的代码在不同版本的 Node.js 中出现不同的执行结果,需要特别注意。