每当我听到人们谈论Node.js时,就会出现很多关于究竟是什么,这项技术有什么用处,以及其未来的问题。

让我们试着解决第一部分。回答这个问题最简单的方法是列出许多 Node 技术上的定义:

  • Node.js 是一个基于 Chrome 的 V8 JavaScript 引擎构建的 Javascript 运行时环境。
  • Node.js 使用事件驱动的非阻塞 I/O 模型,使其轻量且高效。
  • Node 包生态系统(npm)是全世界最大的开源库生态系统。

但是,这些答案并不能令我满意,因为有些东西不见了。在读了上面的要点后,你可能会认为 Node.js 只是另一种 JavaScript 技术,但是如果你想要真正的理解它,最重要的是分析它是如何进行异步操作的和它的非阻塞 I/O 系统。

这是每个 Web 开发人员应该必备的知识。

准确的理解 Node 在幕后的工作原理,不仅会对这项技术了解的更多,还能够激发那些刚刚开始学习但还没深入使用的人们的兴趣。

对于已经是该领域的专业人士来说,了解它的内部和外部将使你成为一个全新、前沿的开发人员,可以根据你的需求去提高其性能。

因此,为了挖掘 Node 的世界,我们将检视其核心部分:事件循环,实际上它是负责其非阻塞 I/O 模型的部分。

A brief refresh on threads

简要刷新线程

在深入了解事件循环之前,我想先在线程上花一些时间。如果你想知道这样做的必要性,我会告诉你是为了更好地理解一个概念,我们必须先在自己的脑海中形成一个术语表,这将有助于我们识别系统的每一个部分。我们会在稍后阅读有关事件循环如何工作,以及如何将线程的概念应用于它的内容时,这最终将具有很大的优势。

每当我们运行一个程序时,就会为它创建一个实例,并且有一些内部调用线程与该实例相关。线程可以看作是我们的 CPU 必须执行的操作单元。许多不同的线程可以与程序的单个进程相关联。下面这个图可以帮你在脑海中形成这个想法:

Node.js 事件循环的完整指南-LMLPHP

线程的简图

在讨论线程时最重要的一点是:我们的机器如何确定在什么时候处理哪个线程?

众所周知,我们机器的资源是有限的(CPU,RAM),因此正确的决定怎样分配它们是非常重要,换一种说法是,哪些操作优先于其他操作。这必须要做到,同时还要确操作不能消耗太多的时间 —— 没有人喜欢运行速度慢的电脑。

用于解决分配问题的机制称为 scheduling,它由操作系统中的调度程序管理。这背后的逻辑可能非常复杂,但总而言之,我们可以将执行此操作的两种主要方式组合在一起:

  • 多核机器:为不同的核心分配不同的线程。

Node.js 事件循环的完整指南-LMLPHP

多核机器如何处理线程

  • 使用可减少空置时间的优化逻辑: 这是最实用的方法。如果仔细研究一下线程是如何工作的,我们将看到 OS 调度程序可以识别 CPU 什么时等待其他资源执行一个作业,由此可以分配它来同时执行其他操作。这通常发生在代价非常昂贵的 I/O 操作上,例如从硬盘读取数据。

事件循环

现在我们已经对线程如何工作有了基本的了解,接下来解决 Node.js 事件循环逻辑。通过本文,你将了解前面那些解释背后的原因,每一条都会对应到正确的位置上。

每当运行 Node 程序时,都会自动创建一个线程。这个线程是整个代码唯一执行的地方。在其中生成了一个被称为事件循环的东西。这个循环的作用是安排我们唯一的线程应该在什么时间点执行哪些操作。

详细的说明

现在让我们尝试模拟事件循环的工作原理及其工作方式。首先假设我们正在用名为 myProgram 的文件为 Node 提供信息,然后详细了解事件循环将对其进行的操作。

Node.js 事件循环的完整指南-LMLPHP

特别是,我将首用一个简短的图来解释,说明在事件循环 tick 过程中发生的事情,然后再以更深入的方式探讨这些阶段。

Node.js 事件循环的完整指南-LMLPHP

步骤1:performChecks

不应该单纯的认为事件循环实际上是一个循环。它有一个特定的条件,用来确定循环是否需要再次迭代。事件循环的每次迭代都被称为一个 tick

事件循环执行 tick 的条件是什么?

每当执行程序时,我们都会进行一系列需要执行的操作。这些操作主要分为三种类型:

  • 等待定时器操作(setTimeout()setInterval()setImmediate()
  • 等待处理中的操作系统任务
  • 等待需要长时间运行的操作

我稍后会详细介绍这些内容;现在让我们记住,只要其中一个操作处于挂起状态,事件循环就会执行一个新的 tick。

步骤2:执行一个 tick

对于每个循环迭代,可以分为以下阶段:

  • 阶段1: Node 查看其内部的挂起计时器集合,并检查传递给 setTimeout()setInterval() 的回调函数是否准备好在计时器过期的情况下被调用。
  • 阶段2: Node 查看其待处理 OS 任务的内部集合,并检查哪些回调函数已准备好被调用。一个例子是从机器的硬盘驱动器中完成了对文件的检索。
  • 阶段3: Node 暂停其执行,等待新事件发生。新事件包括:新的计时器完成,新的OS任务完成,新的待处理操作完成。
  • 阶段4: Node 检查是否已经准备好调用与 setImmediate() 函数相关函数。
  • 第5阶段: 管理关闭事件,用于清理程序状态。

关于事件循环的常见问题和错误观点

Node.js 是完全单线程的吗?

这是对 Node.js 的一种非常普遍的误解。 Node 运行在单个线程上,但是 Node.js 标准库中包含的一些函数并不是(例如 fs 模块函数),他们的逻辑运行在 Node.js 线程之外。这样做是为了保证程序的速度和性能。

这些其他线程运行在哪里?

Node.js 会使用名为 libuv 的特殊库模块来执行异步操作。此库还与 Node 的后台逻辑一起使用,用来管理被称为 libuv 线程池 的特殊线程池。

这个线程池由四个线程组成,用于委派对事件循环来说太重的操作。长时间运行的任务对于事件循环而言代价过于昂贵。

那么事件循环是一种类似栈的结构?

从这个意义上说,虽然在上述过程中涉及一些类似栈的结构,但更精确的答案是事件循环由一系列的阶段所组成,每个阶段都有自己的特定任务,所有阶段都以循环重复的方式去处理。如果想要知道关于事件循环确切结构的更多信息,请查看此演讲

结论

了解事件循环是使用 Node.js 的重要部分,无论你是想获得有关此技术的更多见解,了解如何提高其性能,还是找到学习新工具理由。


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

Node.js 事件循环的完整指南-LMLPHP

欢迎继续阅读本专栏其它高赞文章:


07-26 20:58