Node is a runtime environment for executing JavaScript Code.
Node 既不是一种语言,也不是一个框架,而是一个能执行 JavaScript 代码的运行时环境。

最初的时候,Javascript 只能运行在浏览器中,靠的是 JS 引擎把 JavaScript 代码转成浏览器能识别的机器码,并且不同的浏览器用的是不同的引擎,如 IE 使用 Charkra,Firefox 使用 SpiderMonkey,Chrome 使用 V8,因此,有时候 js 在不同的浏览器中运行会有不同的效果。
这些 Javascript 引擎都遵循 ECMAScript 标准,Javascript 则是 ECMAScript 的一个方言版本,能够被web浏览器和许多其它的应用支持。

浏览器就是一个能够运行 Javascript 代码的运行时环境,我们知道在 js 中,我们能够访问全局变量 window
、document
,这些变量能够让我们与代码所运行的环境进行交互。

之后,Node的创始人 Ryan Dahl 有了一个大胆的想法,想着如果 javascript 在浏览器以外的地方也能运行就好了,因此,他就把速度最快的 javascript 引擎——Google的 V8 引擎嵌入到了一个C++程序中,并且把这个程序叫做 Node。因此,跟浏览器类似,Node 也是一个 JavaScript 代码的运行时环境,它包含了一个JS引擎,能够执行 JavaScript 代码。但是,跟浏览器不同的是,它还提供了能够支持其它功能的一些对象,没有了能够获取 DOM 的 document
对象(document.getElementById()
),但是能够进行文件或者网络操作(fs.createFile()
、http.createServer()
等),这些操作就需要借助于其它的一些库来实现,如 libuv。
一、Node 目录及架构体系

在 github 中,我们可以看到 node 库的目录,其中:
- deps:包含了node所依赖的库;
- lib:包含了我们在项目中引入的用 javascript 定义的函数和模块;
- src:lib 库对应的C++实现。

在 Node 官方文档的 Dependencies 中,我们也可以看到一些具体的所依赖的库的作用。
- v8 引擎的作用就是将 js 转成 C++。
- libuv 用于在C++中处理并发和进程构建,具有跨平台和异步能力。
- c-ares:提供了异步处理 DNS 相关的能力。
- http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。
接下来,我们就主要看两个依赖库:V8 和 Libuv。
V8
谷歌开源的 JavaScript 引擎,目的是使 JavaScript 能够在浏览器之外的地方运行。前面说过,Javascript引擎是一个能够将 Javascript 语言转换成浏览器能够识别的低级语言或机器码的程序。
Libuv
C++的开源项目,使 Node 能够访问操作系统的底层文件系统(file system),访问网络(networking)并且处理一些高并发相关的问题。
那么问题来了,既然 V8 能够让我们使用 JavaScript,libuv 给了我们一些操作系统、网络等层面的访问能力,我们还需要 Node 干嘛呢?

V8 大约70%由 C++ 实现,30%由 JavaScript 实现。
Libuv 100% 由 C++ 实现。
原因不难理解:
(1)因为V8 和 libuv 都并非是用 JavaScript 写的,对于我们前端儿来说,写 C++ 是头疼的事情,而 Node 为我们提供了一个很好的接口,用来将 javascript 应用程序的 javascript 端与运行在我们计算机上的实际 c++ 关联起来,从而实际地解释和执行 javascript 代码。
(2)Node 封装了一系列的 API 供我们使用,并且提供了一致的接口。
二、模块实现
让我们用实际的例子来说明一下 Node 到底是如何运行的:
- 选择 Node standard libary 中的一个函数;
- 在 node 源码中找到它的实现;
- 看下 V8 和 Libuv 是如何被用来实现函数功能的,即 node 是如何在 V8 和 Libuv 中利用和包装功能的。
选择一个函数:scrypt.js
scrypt.js 是 Crypto 模块中的一个函数,Crypto 模块通常用于对密码进行hash化处理。
在源码中,我们主要关注两个文件夹:
-
Lib —— 包含了我们在项目中引入的所有函数和模块的 JS 定义——JS side of node project。
-
Src —— 在这个文件夹里面是所有函数的 c++ 实现,是 Node 实际导入 Libuv 和 V8 项目的地方,也是我们正在使用的所有函数和模块实际实现的地方,比如FS模块、Http模块等等。
在 lib 文件夹下找到 scrypt 的函数实现: node/lib/internal/crypto/scrypt.js
。
在这个 javascript 文件中,包含了对该函数的JS定义。这是 Node 标准库中的函数,就跟我们在任何 javascript 文件中编写的函数一样。
在
scrypt.js
文件中,你会发现internalBinding()
函数,之前的版本是Process.binding()
,现在 node 团队将其改为了internalBinding()
,因为它们现在无法从用户空间访问,而只能从NativeModule.require()
获得。

C++ binding Loaders
-
process.binding(): 已成为历史的 c++ 绑定加载程序,可以从用户空间访问,因为它被附加到了全局对象上。这些 c++ 绑定是通过
NODE_BUILTIN_MODULE_CONTEXT_AWARE()
创建的,并且它们的 nm_flags 设置为NM_F_BUILTIN
。我们无法确保这些绑定的稳定性,因此需要时常处理它们引起的兼容性问题。 -
process._linkedBinding(): 用于在其应用程序中添加额外的c++绑定。这些c++绑定可以通过带有
NM_F_LINKED
标志的NODE_MODULE_CONTEXT_AWARE_CPP()
进行创建。 -
internalBinding(): 私有的内部c++绑定加载程序,用户无法访问,只能通过
NativeModule.require()
获得。这些c++绑定通过NODE_MODULE_CONTEXT_AWARE_INTERNAL()
进行创建,并且它们的nm_flags 设置为NM_F_INTERNAL
。
内部 JavaScript 模块加载器:NativeModule
该模块是用于加载 lib/**/*.js
和 deps/**/*.js
中的JavaScript核心模块的最小模块系统。
所有核心模块都通过由 js2c.py
生成的 node_javascript.cc
编译成 Node 二进制文件,这样可以更快地加载它们,而不需要I/O成本。
这个类使 lib/internal/*
、deps/internal/*
模块和 internalBinding()
在默认情况下对核心模块可用,并且允许核心模块通过 require('internal/bootstrap/loaders')
来引用自身,即使这个文件不是用 CommonJS 风格编写的。
Process.binding / InternalBinding 实际上是C++函数,是用于将Node标准库中C++端和Javascript端连接起来的桥梁。
Process.binding() / internalBinding() 是如何工作的?
它们是 Node的 JS 端和 C++ 端之间的桥梁,也是 Node 为你实现大量内部工作的地方。你的很多代码最终都是依赖于c++代码的。
现在,让我们看一下在 src 文件夹中如何实现 Node 的 c++ 端:node/src/node_crypto.cc
。
node_crypto.cc —— crypto 模块所依赖的并位于 Node 的 c++ 部分的实际代码。
在该文件的最后,你会看到对 C++ setMethod()
的导出,这行代码最终将由internalBinding() / process.binding()
进行调用。

这在某种程度上是将Node的Javascript端与Node的c++端连接起来。
这是我们所写的函数实际实现的地方,100%纯c++代码。👊
现在我想你应该已经了解了,当我们运行Javascript代码,实际上它内部依赖的是c++代码。

现在,你可能对 V8 和 Libuv 是如何发挥作用的很好奇,那么,接下来,我们就一起来看一下。
在文件的顶部,我们可以看到有这么些代码:

这里引入了 V8::Array
等类型,可以看到,在 node 源码中使用 V8 的目的,本质上是作为一个完整的中介,允许在 JavaScript 中所定义的值被转换成等价的 C++ 值。
所有的 V8 语句都导入了 JS 概念的 C++ 定义。比如 C++ 对 JS 中的 False、Integer、null 或者 string 等的理解。
这就是实际的 V8 项目发挥作用的地方。V8 用于将我们在不同程序中的写的 JS 类型的值,如 Boolean值、false值、null值、object值等转换成C++中的值。
另一方面,Libuv 也在这里使用,但是不太容易被检测到,我们可以搜索 uv
,从而找到使用的地方。在 node_crypto.cc
的例子中,Libuv 用于 C++ 端的并发和进程处理。

看到这里,我想大家对 Node 的内部工作原理应该有一些了解了。
最后,总结成一张图:

网友评论