自2009年11月8日Ryan Dahl在欧洲JSConf上介绍Node.js以来,它已在整个技术行业得到广泛使用。Netflix,Uber和LinkedIn等公司对Node.js可以承受大量流量和并发性的说法表示信服。
掌握了基本知识,Node.js的初学者和中级开发人员在很多事情上都苦苦挣扎:“这只是运行时!” “它具有事件循环!” “ Node.js像JavaScript一样是单线程的!”
尽管其中一些说法是正确的,但我们将更深入地研究Node.js运行时,了解它如何运行JavaScript,查看它是否实际上是单线程的,最后,更好地了解其核心依赖项V8和libuv之间的互连。 。
先决条件
- JavaScript的基础知识
- 熟悉Node.js语义(
require
,fs
)
什么是Node.Js?
假设许多人对Node.js抱有信念可能是很诱人的,最常见的定义是它是JavaScript语言的运行时。考虑到这一点,我们应该理解导致这一结论的原因。
Node.js通常被描述为C ++和JavaScript的组合。C ++部分由运行低级代码的绑定组成,这些绑定使访问连接到计算机的硬件成为可能。JavaScript部分将JavaScript作为其源代码,并在一种流行的语言解释器V8引擎中运行它。
有了这种了解,我们可以将Node.js描述为一个独特的工具,该工具结合了JavaScript和C ++以在浏览器环境之外运行程序。
但是实际上我们可以称它为运行时吗?为了确定这一点,让我们定义什么是运行时。
什么是运行时?https://t.co/eaF4CoWecX
-Christian Nwamba(@codebeast)2020年3月5日
在他对StackOverflow的回答之一中,DJNA将运行时环境定义为“执行程序所需的一切,但无需任何工具即可对其进行更改”。根据这个定义,我们可以放心地说,我们运行代码(无论使用任何语言)时发生的所有事情都在运行时环境中运行。
其他语言也有自己的运行时环境。对于Java,它是Java运行时环境(JRE)。对于.NET,它是公共语言运行库(CLR)。对于Erlang,它是BEAM。
但是,其中一些运行时具有依赖于它们的其他语言。例如,Java具有Kotlin,这是一种编程语言,可以编译为JRE可以理解的代码。Erlang有长生不老药。我们知道.NET开发有许多变体,它们都在CLR中运行,称为.NET Framework。
现在我们知道运行时是为程序成功执行提供的环境,并且我们知道V8和大量C ++库使Node.js应用程序得以执行。Node.js本身是实际的运行时,它将所有内容绑定在一起以使这些库成为一个实体,并且它仅理解一种语言-JavaScript,而与Node.js所使用的语言无关。
Node.Js的内部结构
当我们尝试index.js
使用command从命令行运行Node.js程序(例如)时node index.js
,我们正在调用Node.js运行时。如前所述,该运行时由两个独立的依赖项V8和libuv组成。
V8是由Google创建和维护的项目。它需要JavaScript源代码,并在浏览器环境之外运行。当我们通过node
命令运行程序时,Node.js运行时会将源代码传递给V8以执行。
libuv库包含用于对操作系统进行低级访问的C ++代码。V8默认不提供诸如联网,写入文件系统和并发之类的功能,而V8是运行我们的JavaScript代码的Node.js的一部分。借助其库集,libuv在Node.js环境中提供了这些实用程序以及更多功能。
Node.js是将两个库结合在一起的粘合剂,因此成为唯一的解决方案。在脚本的整个执行过程中,Node.js会了解将控制权传递给哪个项目以及何时传递。
服务器端程序有趣的API
如果我们研究一下JavaScript的一些历史,就会知道它的意思是向浏览器中的页面添加一些功能和交互。在浏览器中,我们将与构成页面的文档对象模型(DOM)的元素进行交互。为此,存在一组API,统称为DOM API。
DOM仅存在于浏览器中。它是用来渲染页面的解析文件,它基本上是用称为HTML的标记语言编写的。而且,浏览器存在于一个窗口中,因此存在于一个window
对象中,该对象充当JavaScript上下文中页面上所有对象的根。该环境称为浏览器环境,它是JavaScript的运行时环境。
在Node.js环境中,我们没有页面,也没有浏览器,这使我们对全局window对象的了解无效。我们所拥有的是一组与操作系统交互的API,以为JavaScript程序提供其他功能。这些API对于Node.js的(fs
,path
,buffer
,events
,HTTP
,等),因为我们有他们,只存在于Node.js,它们通过Node.js的(本身运行时),以便我们可以运行编写的程序提供Node.js。
实验:如何fs.writeFile
创建新文件
如果V8是为了在浏览器之外运行JavaScript而创建的,并且Node.js环境与浏览器没有相同的上下文或环境,那么我们将如何做类似访问文件系统或制造HTTP服务器的事情?
例如,让我们看一个简单的Node.js应用程序,该应用程序将文件写入当前目录中的文件系统:
如图所示,我们正在尝试将新文件写入文件系统。JavaScript语言不提供此功能。它仅在Node.js环境中可用。如何执行?
为了理解这一点,让我们浏览一下Node.js代码库。
转到Node.js的GitHub 存储库,我们看到了两个主要文件夹src
和lib
。该lib
文件夹具有JavaScript代码,该代码提供了漂亮的一组模块,默认情况下,每个Node.js安装中都包含这些模块。该src
文件夹包含libuv的C ++库。
如果我们查看lib
文件夹并浏览fs.js
文件,我们将看到它充满了令人印象深刻的JavaScript代码。在1880行,我们将注意到一条exports
声明。该语句导出我们可以通过导入fs
模块访问的所有内容,并且可以看到它导出了名为的函数writeFile
。
搜索function writeFile(
(定义函数的位置)将导致我们转到第1303行,在该行中,我们看到该函数定义了四个参数:
在第1315和1324行,我们看到writeAll
在进行一些验证检查之后调用了一个函数。我们在同一文件的1278行中找到此功能。fs.js
还有趣的是,该模块正在尝试自行调用。我们在1280行看到它正在调用fs.write
。寻找write
功能,我们将发现一些信息。
该write
函数从571行开始,大约运行42行。我们在此函数中看到一个重复出现的模式:binding
如第594和612行所示,它在模块上调用函数的方式。binding
模块上的功能不仅在此功能中被调用,而且实际上在fs.js
文件文件中导出的任何功能中都被调用。必须有一些特别之处。
在文件的最上方第58行binding
声明该变量,然后在GitHub的帮助下单击该函数调用即可显示一些信息。
该internalBinding
功能可以在名为loaders的模块中找到。加载程序模块的主要功能是加载所有libuv库,并将它们通过V8项目与Node.js连接。它是如何实现的,这是很神奇的,但是要了解更多信息,我们可以仔细查看模块writeBuffer
调用的函数fs
。
我们应该查看它与libuv的连接位置以及V8的输入位置。在加载程序模块的顶部,一些好的文档说明了这一点:
我们在这里了解到的是,对于从binding
Node.js项目的JavaScript部分中的对象调用的每个模块,该src
文件夹的C ++部分中都有与之等效的模块。
从我们的fs
浏览中,我们看到执行此操作的模块位于中node_file.cc
。文件中定义了可通过模块访问的每个功能;例如,我们在writeBuffer
上线2258。在C ++文件中该方法的实际定义在第1785行。同样,可以在第1809和1815行中找到对libuv进行实际写入文件的部分的调用,其中libuv函数uv_fs_write
被异步调用。
我们从这种理解中学到什么?
就像许多其他解释型语言运行时一样,Node.js的运行时也可以被黑客入侵。有了更多的了解,我们就可以通过查看源代码来完成标准发行版无法完成的工作。我们可以添加库来更改某些函数的调用方式。但最重要的是,这种理解是进一步探索的基础。
Node.Js是单线程的吗?
坐在libuv和V8上,Node.js可以访问浏览器中运行的典型JavaScript引擎所没有的一些其他功能。
在浏览器中运行的任何JavaScript都将在单个线程中执行。程序执行中的线程就像一个黑匣子,位于执行该程序的CPU上。在Node.js上下文中,某些代码可以在我们的计算机可以承载的尽可能多的线程中执行。
为了验证这一特定要求,让我们探索一个简单的代码片段。
在上面的代码段中,我们尝试在当前目录的磁盘上创建一个新文件。为了了解这可能需要多长时间,我们添加了一个基准测试来监视脚本的开始时间,这为我们提供了创建文件的脚本的持续时间(以毫秒为单位)。
如果运行上面的代码,我们将得到如下结果:
这非常令人印象深刻:仅0.003秒。
但是,让我们做一些非常有趣的事情。首先,让我们复制生成新文件的代码,并更新log语句中的数字以反映它们的位置:
如果我们尝试运行此代码,我们将感到不解。这是我的结果:
首先,我们将注意到结果不一致。其次,我们看到时间增加了。发生了什么?
低级任务被委派
众所周知,Node.js是单线程的。Node.js的某些部分是用JavaScript编写的,而其他则是用C ++编写的。Node.js使用了我们在浏览器环境中熟悉的事件循环和调用堆栈的相同概念,这意味着Node.js的JavaScript部分是单线程的。但是需要与操作系统对话的低级任务不是单线程的。
当Node.js将调用识别为针对libuv的调用时,它将此任务委托给libuv。在操作中,libuv的某些库需要线程,因此在需要时在执行Node.js程序时使用线程池。
默认情况下,libuv提供的Node.js线程池中有四个线程。我们可以通过process.env.UV_THREADPOOL_SIZE
在脚本顶部调用来增加或减少该线程池。
我们的文件制作程序会发生什么
看来,一旦我们调用代码创建文件,Node.js就会到达其代码的libuv部分,这将为该任务分配一个线程。libuv中的此部分在处理文件之前会获取有关磁盘的一些统计信息。
此统计检查可能需要一段时间才能完成;因此,线程将被释放以执行其他一些任务,直到完成统计检查为止。检查完成后,libuv节将占用任何可用线程,或者等待直到某个线程变得可用。
我们只有四个调用和四个线程,因此有足够多的线程可以使用。唯一的问题是每个线程处理任务的速度如何。我们将注意到,第一个进入线程池的代码将首先返回其结果,并且在运行其代码时会阻塞所有其他线程。
结论
现在我们了解了Node.js是什么。我们知道这是一个运行时。我们已经定义了什么是运行时。我们已经深入研究了由Node.js提供的运行时的组成部分。
我们已经走了很长一段路。然后,通过对GitHub上Node.js存储库的少量浏览,我们可以按照此处采取的相同过程来探索我们可能感兴趣的任何API。Node.js是开源的,所以我们可以肯定会进入源代码,不是吗?
即使我们已经谈到了Node.js运行时中发生的几个低级问题,我们也不能假设我们已经知道了一切。以下资源指向一些我们可以用来建立知识的信息:
- Node.js简介
作为一个官方网站,Node.dev解释了什么是Node.js及其包管理器,并列出了在其之上构建的Web框架。 - “ JavaScript&Node.js ”,Node入门书Manuel Kiessling的
这本书在警告浏览器中的JavaScript与Node.js中的JavaScript不一样之后,解释了Node.js做了出色的工作,即使两者都是用相同的语言编写。 - 入门 Node.js这本入门书超出了对运行时的解释。它介绍了有关软件包和流以及如何使用Express框架创建Web服务器的知识。
- LibUV
这是Node.js运行时支持的C ++代码的正式文档。 - V8
这是JavaScript引擎的官方文档,它使使用JavaScript编写Node.js成为可能。