什么是 Node.js?

Node.js是一个基于Google开源V8 JavaScript引擎构建的JavaScript运行时。Node.js 可以在服务器端执行 JavaScript 代码,从而可以创建快速、可扩展、高性能的网络应用程序。

Node.js 的架构和了解 V8 引擎

Node.js的依赖项可以正常工作/运行。最重要的是 v8 引擎和 libuv。

编写代码时,机器不会直接理解我们的代码。机器必须将其转换为机器代码才能理解我们的命令。浏览器中有引擎可以将我们的代码理解为机器代码。不同的浏览器有不同的引擎来理解 JavaScript 代码并将其转换为机器代码。

  • Google Chrome: V8
  • Mozilla Firefox: Spider Monkey(蜘蛛猴)
  • Apple Safari: JavaScriptCore
  • Microsoft Edge:脉轮
  • Opera: Blink(基于与 Google Chrome V8 相同的引擎)

最强大的是 Chrome 的 v8 引擎。

在服务器端,必须有一些东西可以将 JavaScript 代码转换为机器代码。为此,Node.js中使用了超高速V8引擎。现在,通过这种方式,Node.js可以将JavaScript代码转换为机器代码。这就是 Node 中 V8 引擎的用例。

libuv 库

V8引擎并不孤单,无法运行像Node.js这样的服务器端框架。这是另一个名为“libuv”的关键库。它是一个开源库,非常关注异步 IO(输入/输出)。

通过libuv,Node.js可以访问底层计算机操作系统,文件系统,与网络相关的东西等等。

“libuv” 实现了 Node.js 的两个关键特性 :

  1. 事件循环,用于处理简单的任务,如回调或网络 io
  2. 线程池,用于处理繁重的工作,如访问文件或文件压缩。

Node.js 不完全是用 JavaScript 编写的

直觉地认为 Node.js 在幕后只有 JavaScript 代码,因为 Node.js 是一个 JavaScript 运行时。但事实并非如此。正如所说,libuv 是 Node 的重要组成部分,而 libuv 完全是用 C++ 编写的。另一方面,V8也是用JS和C++编写的。因此,Node不仅是用JavaScript编写的,而且还是用C++编写的。但是有一些抽象层,所以可以通过纯 JavaScript 函数访问 Node 中的所有函数和所有内容。不必搞砸 C++ 或任何底层代码。例如,对于文件读取,编写一个纯 js 函数来从文件系统中读取,该功能是用 C++ 的 libuv 编写的。

需要注意的重要一点是,Node.js 不仅依赖于 V8 引擎和 libuv。还有其他库,比如 HTTP 解析器(用于解析 HTTP)、c-ares(用于处理 DNS 请求)、OpenSSL(用于加密)、zlib(用于文件压缩)等。

线程和线程池

每当使用 Node.js 时,都会在我们的计算机上启动一个节点进程。此进程表示当前正在执行的程序。

在该进程中,节点在单个线程中运行。线程就像是大程序中的小程序,是可以同时执行多个任务的指令序列。它就像一个执行代码的盒子。

清楚地理解多线程和单线程的概念至关重要。多线程支持同时执行多个操作,其中多个任务可以同时在后台运行。另一方面,单线程一次只允许执行一个操作。换句话说,它以线性顺序运行,不能同时执行多个操作。

重要的是要了解 Node 在单个线程上运行,无论有多少用户。这意味着,如果单个线程被阻塞,整个应用程序可能会受到影响。因此需要格外注意可能阻塞线程的操作。

Node 在单个线程中运行时会发生什么。

当 Node 应用程序运行时,程序会经历几个阶段。Node.js 初始化程序,执行所有顶级代码,需要必要的模块,然后注册事件回调。

完成所有这些步骤后,事件循环将开始运行。事件循环,应用程序的大部分工作都由此完成。它是应用程序的核心。但是,有些任务可能是资源密集型和繁重的。如果在事件循环中执行,它们可能会阻塞单个线程。在事件循环之外处理这些繁重的任务对于避免这种情况非常重要。

在这里,线程池可以提供帮助,libuv 库提供了它。事件循环将这些繁重的任务卸载到线程池。它发生在幕后。不必担心哪些任务会进入线程池,哪些不会。

线程池有四个独立于单个主线程的附加线程。可以配置一个最多 128 个线程的线程池。但是,通常,我们不需要。四个线程就足够了。

下面是从事件循环卸载到线程池的一些任务示例:

使用文件系统 API 的任务:当执行读取或写入文件等操作时,这些任务可能需要大量时间。它们可能会阻塞单个线程并导致性能问题。

密码学相关任务:密码学涉及复杂的数学计算。在事件循环中执行这些操作可能会减慢我们的应用程序速度。

压缩任务:当需要压缩图像或视频等大型数据时,这些任务可能会花费大量时间,并且还会减慢事件循环的速度。因此,这些任务被卸载到线程池以防止这种情况发生。

DNS查找:当需要将域名解析为IP地址时,是通过DNS查找过程完成的。此过程还可能需要一些时间并阻塞事件循环,这可能会导致性能问题。因此,为了防止这种情况,这些任务被卸载到线程池中,在那里它们可以并发运行而不会阻塞主线程。

事件循环及其在 Node.js 中的实现

现在,进入了 Node.js 进程。此进程包含运行事件循环的线程。事件循环是 Node.js 进程的重要组成部分。它负责执行构成 Node.js 应用程序的回调函数中的所有代码。

一些任务也可以卸载到线程池中,以便高效处理。但是,事件循环是 Node.js 运行时的核心机制。Node.js 基于回调驱动的架构。这意味着一旦某个事件完成或发出,就会调用函数。发生这种情况是因为 Node.js 的事件驱动架构。

事件驱动架构

事件驱动架构是一个系统,其中事件由事件循环发出、拾取和处理。然后,事件循环对该事件执行关联的回调函数。

例如,新的 HTTP 请求、计时器过期或已完成的文件读取或写入文件操作等事件将发出事件,然后事件循环将拾取这些事件。然后,它通过执行关联的回调函数来处理这些事件。事件循环观察事件并在后台执行必要的编排。

事件循环在幕后的实际操作方式:

一旦 Node.js 应用程序启动,事件循环就会开始运行。事件循环有几个阶段。每个阶段都有自己的回调队列。有人可能会说事件循环只有一个回调或事件队列。但它有多个阶段,每个阶段都有自己的回调队列。

事件循环的四个最重要的阶段:

1. 处理过期的计时器回调 (setTimeout()):第一阶段涉及从过期的计时器执行回调。如果计时器在任何其他阶段过期,则其关联的回调函数将在当前阶段完成后立即执行,并且事件循环返回到其第一个阶段。

2. I/O 轮询和 I/O 回调的执行:节点 IO 意味着网络或文件系统的东西,如文件读取、写入新文件等,轮询意味着 – 寻找准备处理的新 IO 事件并将它们放在回调队列中。此阶段是执行 99% 的应用程序代码的地方。

3. 执行 SetImmediate 回调:这是一种特殊类型的计时器,允许在执行 I/O 回调后立即执行代码。

4. 执行关闭回调:事件循环处理此阶段的所有关闭事件。这包括关闭 Web 服务器或 Web 套接字等事件。

除了这四个阶段之外,还需要注意另外两个队列:

process.nextTick() 队列:此队列在当前阶段完成后立即执行其回调。它类似于 SetImmediate 回调。唯一的区别是 SetImmediate 回调仅在执行 I/O 回调后立即执行,而 process.nextTick() 在任何阶段之后立即执行。

用于解析 promise 的微任务队列:此队列在当前阶段完成后立即执行其回调,就像 process.nextTick() 队列一样。如果这两个队列中的任何一个中有任何回调,它们将在当前阶段完成后立即执行,而不是等待整个事件循环完成其四个阶段。例如,如果 promise 在过期计时器的回调运行时解析并返回 API 调用中的数据,则其回调将在计时器的回调完成后立即执行。

在前四个阶段之后,事件循环会检查这两个队列中是否有任何回调。如果有,它们将立即被执行。这样就完成了事件循环的一个时钟周期。即时报价定义为循环的一个周期。一个周期后,Node.js 运行时检查是否有任何待处理的计时器或 I/O 任务,或任何阶段的任何回调。如果有,循环将再次运行。否则,应用程序将退出。

由于事件循环,异步编程在 Node.js 中成为可能,这使得事件循环成为 Node.js 最重要的功能。它允许 Node.js 在单个线程中运行,使其轻量级和快速,但也存在一些潜在风险。由于 Node.js 是一个单线程程序,因此可能会意外阻止应用程序。因此,在编写代码时要格外小心,以避免意外阻止应用程序,这一点很重要。

避免阻塞事件循环的步骤

以下是一些避免在 Node.js 中阻塞代码的提示:

  • 避免使用函数的同步版本;在任何回调函数之外编写任何必要的同步代码。这样,代码将在事件循环开始之前执行。

  • 避免复杂的计算,例如 Node.js 中的嵌套循环,因为它们可能会导致应用程序被阻塞。

  • 处理大型 JSON 对象时要小心,因为解析或字符串化它们可能会变得非常耗时。

  • 避免使用复杂的正则表达式,因为它们需要大量的处理能力,这些处理能力可能会占用大量资源并减慢事件循环的速度。如果使用复杂的正则表达式,执行可能需要很长时间。这就是为什么还建议将大型正则表达式分解为更小、更简单的正则表达式,以便更有效地执行。这将确保事件循环继续平稳运行,并确保应用程序保持响应。

  • 不要在事件循环中执行任何 CPU 密集型任务。它可能导致应用程序被阻止。以下是一些 CPU 密集型任务示例-

  • 繁重的计算,例如数学计算和科学模拟

  • 图像和视频处理,例如调整图像和视频的大小、裁剪和过滤

  • 加密操作,例如数据的加密和解密

  • 数据压缩和解压缩

  • 如果想在 Node.js 中执行这些任务,请务必小心。不得不采取额外的步骤来最大程度地减少它们对事件循环的影响。这可以通过手动将这些任务卸载到单独的进程或线程或使用为执行这些类型的操作而优化的外部库来完成。

此外,以不会对事件循环施加压力的方式构建应用程序代码也很重要,例如避免使用复杂的算法、最大限度地减少阻塞操作以及尽可能使用异步编程技术。这将有助于确保我们的 Node.js 应用程序即使在重负载下也能保持快速、高效和响应。