Skip to main content

如何实现的

Node.js 既不是一门语言,也不是框架,而是一个跨平台的 JavaScript 运行时环境,由 Ryan Dahl 于2009年开发。

Node.js 的架构

Node架构

Google V8 引擎是用 C++ 编写的开源高性能 JavaScript 和 WebAssembly 引擎,主要用于高效执行 JavaScript 代码。

Libuv 是一个跨平台的异步 I/O 库,也是由 C++ 编写。

Libuv 的架构

Libuv

Event Loop

Node.js 工作在事件驱动模型之上,这个模型包含一个事件分发器和一个事件队列,这种模型也被称为反应器模式(Reactor Pattern)。

事件分发器

事件分发器在不同操作系统下有不同的底层实现方案,Node.js 封装了不同平台的实现,从而实现了 Event Loop 机制,进而使得开发者能够以异步的方式进行网络编程。

  • Liunx 下的 epoll
  • OSX 下的 Kqueue
  • Solaris 下的 event ports
  • Windows 下的 IOCP

事件队列

事件队列是一个不断有事件出入队列,并被执行器不断轮询的数据结构。

在 Node.js 中,并非只有一个队列,而是存在多个队列。

Libuv 为实现 Event Loop 建立了4个队列

  • Timeout & Interval,如 setTimeout 或 setInterval 触发的回调
  • I/O Events,如 fs.readFile 指定的文件被读取完成时的回调
  • Immediates,如 setImmediate 添加的回调
  • Close Handlers,如 close 事件的回调

除了由 Libuv 实现的队列外,Node.js 本身还实现了2个中间队列

  • Next Tick,如 process.nextTick 添加的回调
  • Promise Microtasks,如 Native Promise 产生的回调

队列协同工作

Event Loop

如图所示,Event Loop 从 Timeout & Interval 队列开始,顺序并循环遍历每个队列,执行器轮询一个队列的过程被称作一个阶段,当所有队列皆为空,且没有其它未完成的后台任务时,程序结束。

此外,从 Node.js v11 版本开始,在任意两个阶段中间,Node.js 还会执行自身实现的2个中间队列中的全部回调。

后台任务,指的是 Node.js 会将一些 CPU 密集型的异步任务交由后台执行,如 crypto 和 zlib 下的一些方法。

理解了这个原理,就能理解下面这段代码的执行顺序

setTimeout(() => console.log('set timeout1'), 0);
Promise.resolve().then(() => console.log('promise1 resolved'));
Promise.resolve().then(() => {
console.log('promise2 resolved');
process.nextTick(() => console.log('next tick inside promise resolve handler'));
});
Promise.resolve().then(() => console.log('promise3 resolved'));
setImmediate(() => console.log('set immediate1'));
process.nextTick(() => console.log('next tick1'));
setImmediate(() => console.log('set immediate2'));
process.nextTick(() => console.log('next tick2'));
Promise.resolve().then(() => console.log('promise4 resolved'));
setTimeout(() => {
console.log('set timeout2');
process.nextTick(() => console.log('next tick inside timmer handler'));
}, 0);
next tick1
next tick2
promise1 resolved
promise2 resolved
promise3 resolved
promise4 resolved
next tick inside promise resolve handler
set timeout1
set timeout2
next tick inside timmer handler
set immediate1
set immediate2

Thread Pool

阻塞操作并非只有网络 I/O,还有文件 I/O 以及基于文件 I/O 的其它服务,如 DNS 等,很多操作系统并没有完全提供这些操作的异步接口。

Node.js 为了实现完全的事件驱动模型,在尽可能利用底层异步非阻塞特性的同时,也不得不维护 Thread Pool 来解决这些问题。

Node.js 也使用 Thread Pool 来处理高成本的任务,包括一些操作系统没有提供的非阻塞 I/O 操作,以及一些 CPU 密集型的任务。

  • I/O 密集型任务
    • DNS
      • dns.lookup()
      • dns.lookupService()
    • 文件系统
      • 所有的文件系统 API,除 fs.FsWatcger() 和那些显式同步调用的 API 之外
  • CPU 密集型任务
    • Crypto
      • crypto.pbkdf2()
      • crypto.scrypt()
      • crypto.randomBytes()
      • crypto.randomFill()
      • crypto.generateKeyPair()
    • Zlib
      • 所有 Zlib 相关函数,除那些显式同步调用的 API 之外