深入了解 Node.js

- Published on

深入了解 Node.js
说明:
- 本文以 Node.js v14.16.0 为参考
- "Node.js"是官方文档的称呼,它也有其他不同的叫法,如:NodeJS/nodeJs,本文使用 Node 代替 Node.js
1. Node 起源与 10 年大事件
Ryan Dahl 是一位 C/C++ 程序员,创造 Node 之前他的主要工作是围绕高性能 Web 服务器进行的。经过尝试后他发现高性能 Web 服务器的要点:
- 事件驱动
- 非阻塞 I/O
明确了主要需求,下一步就是找到合适的开发语言.
2008 年,Google 发布了 Chrome 的新一代引擎 -- V8,
而且,JavaScript 在前端已经有很好的事件驱动应用场景,且 JS 的开发门槛也比 C 语言等低一些。
同时,JS 没有相应的服务端历史包袱。
经过 Ryan Dahl 大佬的一通考量,最终,确定了 JavaScript 来作为开发语言。
Node 10 年大事件:

2. Node 是什么
Node.js 是一个基于 Google V8 引擎
的JavaScript 运行时。
简单点说就是:Node.js 利用 V8 引擎提供了一个执行 JavaScript 的环境。
只不过这个执行环境和浏览器提供的 JavaScript 执行环境不同。
Chrome 与 Node 的区别:

Node 的详细组成:

Node 的组成依赖:
- 核心模块和 C++基础模块(提供接口供我们直接、间接使用)
- V8(C++编写):JS 引擎,负责 JS 的编译、执行、内存管理、垃圾回收等
- libuv(C 语言编写):负责处理异步 I/O(包括事件循环、异步 DNS 解析、异步文件操作、异步 TCP、管理异步的线程池等)
- http-parser(C 语言编写):HTTP 消息解析器
- OpenSSL(C 语言编写):实现了套接字加密功能(SSL、TLS)
- zlib:资料压缩解压
3. Node 如何工作
把 Node 比作一个餐厅,顾客(JS 程序)向服务员点餐(发起任务),服务员(JS 引擎)把下单的菜给到后厨(Chrome API 或 libuv,用于处理异步任务),然后服务员就去服务下一桌顾客了,这种处理了任务请求的过程是异步
的。因为服务员(JS 引擎)一直没闲着。
如果服务员把订单给到厨房,然后等厨房做完所有的菜(如:处理数据请求等 IO 操作),在等待期间不去做任何其他事,最后等厨师把菜做出来后把菜端给客户,这个就是同步
的。

服务员一直在干活,所以服务员是同步(Node 主线程是单任务且同步)的,有顾客到了就处理他们的点餐,这个过程是很快的(V8 很快),而且要必须很快。如果哪一桌顾客点餐花了一个小时(耗时较多的任务,如:CPU 密集型任务视频解码),服务员一直在处理这一桌顾客,这样会影响整个餐厅的运作(程序运行)。

Node 默认就是这种非阻塞(non-blocking)
的 异步架构(Asynchronous Architecture)
.
3.1 Node 系统
Node 系统各主要模块及工作流程

这张图解释了官网对 Node 的主要功能描述:
- Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境
这句话体现在左边那部分,即左侧两列东西。
- Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效
即右侧的 LIBUV 库。
3.2 Node 适用场景
适用场景:
I/O 密集型(I/O intensive): 如 RESTful API 请求
Node 擅长 I/O 密集型的优势主要在于 Node 利用事件循环的处理能力。
Node 面向网络且擅长并行 I/O,不是启动多个线程,为每一个请求服务,
资源占用极少
。
不适用场景:
CPU 密集型(I/O intensive)
CPU 消耗较大的游戏
大数据计算
视频解码
4. I / O
I/O(Input/Output),即输入/输出,通常指数据在存储器(内部和外部)或其他周边设备的输入输出,
是信息处理系统(计算机)与外部世界之间的通信。
计算机读写CPU 缓存、内存、硬盘、获取网络请求都是 I/O.
I/O 是计算机的基础。
计算机执行我们的代码通常是很快的。
但 I/O 绝对是计算机基本操作中最慢的。
系统的各种延时:

上图是系统的各种延时,不包含脚本执行时间。
如果我们的代码中有挺多同步 I/O,可想而知脚本执行时间可能要以 s(秒)来计算了。
《高性能 JavaScript》一书中曾总结过,超过 100ms,用户就会感觉卡顿。
雪上加霜的是,Js 代码的执行和 UI 渲染引擎共用一个线程。
所以,
用户页面显示的时间 > 网络请求时间 + 执行 JS 时间 + 渲染时间。
4.1 工作模式
I/O 工作模式有两种:
- 同步: 调用者会主动等待调用结果
- 异步: 调用者发起一个异步调用,然后立即返回去做别的事;“被调用者”通过状态、通知、回调函数等手段来通知“调用者”。
按照“调用者”线程在等待调用结果时的状态可分为:
- 阻塞:线程被操作系统挂起,直到执行完任务
- 非阻塞:线程不被操作系统挂起,可以处理其他事情
以餐厅点菜场景举例说明一下:
你是服务员(I/O 线程,调用者),有顾客点菜(I/O 操作),你把菜单交给后厨(系统内核,被调用者)。
阻塞:
你把订单交给后厨之后,然后什么都不干,等着后厨做菜
非阻塞:
你把订单交给后厨之后,继续去服务其他顾客
同步:
后厨收到订单后就开始做菜,做完后就递给你
异步:
后厨收到订单后开始做菜,做完后会喊你过来拿
所以可以看出,阻塞与非阻塞是相对于你这个服务员(I/O 线程)而言的,同步与异步是相对于后厨(系统内核)而言的。
阻塞与非阻塞的优缺点:
阻塞 -- 你每接到一个请求都要等很长时间,你作为线程,老板要给你开工资(消耗内存),显然你是在浪费时间(浪费线程资源)
非阻塞 -- 你去服务其他顾客了,但是此时需要不断地(轮询)跑过去问后厨有没有把你的订单做好,这样就会很忙碌
显然,最完美的方案就是异步非阻塞。
可惜的是,目前所有的操作系统都没有完成这么个方案 🤷♂️ 。

aio 是什么?它不就实现了异步非阻塞吗?
不好意思,它实现的异步非阻塞只支持内核 I/O。
Windows 系统同样也没有提供异步非阻塞的 I/O。
5. Reactor 与 Proactor
处理 web 请求通常有两种体系结构,分别为:
thread-based architecture(基于线程的架构)
每来一个请求就开一个线程(PHP)
event-driven architecture(事件驱动模型)
每来一个请求,添加一个事件回调(Node)
事件驱动模型:
- Reactor 模式
Reactor 模式是一种被动的处理,即有事件发生时被动处理;
Reactor 实现相对简单,对于链接多,但耗时短的处理场景高效。
- Proactor 模式
Proactor 实现逻辑复杂;实现优秀的如 windows IOCP,可惜 *nix 没有实现。
适用于异步接收和同时处理多个服务请求的事件驱动程序的场景。
更多关于 Reactor 与 Proactor 的介绍请看下方参考链接 3.
Node 使用的 I/O 模型就是 Reactor。
6. Node 的异步 I/O -- Event Loop🌟
Node 的异步 I/O 模型也叫做 -- 事件循环。
Node 的 Reactor pattern(模型):

过程分析:
- 应用程序向 Event Demultiplexer(事件多路分解器)提交请求来生成新的 I/O 操作。
同时,应用程序还指定了一个处理程序,当操作完成时将调用该处理程序。
向 Event Demultiplexer(事件多路分解器)提交请求是一种非阻塞调用,它立即将控制权返回给应用程序,所以应用程序的执行是同步的。
- 当一组 I/O 操作完成时,事件多路分解器将新的事件推入 Event Queue(事件队列)。
- 此时,Event Loop(事件循环) 遍历 Event Queue(事件队列)中的事件项目
- Event Loop(事件循环)调用和当前事件项目关联的处理程序(回调函数)
- 执行处理程序时,如果有新的 I/O,则执行 5b,会导致一个新的 I/O 循环;如果没有新的 I/O,则会把控制权返回给 Event Loop
- 重复执行 3、4、5 步骤,直到Event Loop中所有项目都被处理完成时,循环将再次阻塞 Event Demultiplexer,直到Event Demultiplexer有新的事件添加到 Event Queue 会再次触发 Event Loop
6.1 Event Demultiplexer(事件多路分解器)
Event Demultiplexer(事件多路分解器)处理具体的 I/O 操作,如 File System
、DataBase
、Computation
等。
Event Demultiplexer 是 libuv 的一部分。
Event Demultiplexer 有一个线程池来处理任务,线程池里有 K 个线程,由 Node 内核来确定。
这个线程池是在启动 Node 时启动的。
线程池里的每个线程可以处理多个 I/O 任务,这是 Node 占用资源少的主要原因之一。
Node 一共有两种线程:🌟🌟🌟
- 一个Event Loop(事件循环)线程(也叫主线程,主循环,事件线程等),启动 Node 就是启动了一个Event Loop(事件循环)线程
- 另一种是工作线程,有 K 个(它们组成工作线程池)
必须知道的是,Node 是单线程是指主线程,但是 libuv 可是多线程的。
6.2 Event Loop (事件循环)
Event Loop 是 Node 的关键组成部分。十分关键的那种!
因为,Event Loop(事件循环)就是 Node 处理非阻塞 I/O 操作的机制。
Event Loop(事件循环)也是在 Node 启动后初始化的。
6.2.1 Event Loop(事件循环)简化概览
事件循环概览图:

事件循环的七个阶段:

说明:
- 宏任务回调函数的同步调用被称为一个 Tick(记号),如:(setTimeout、I/O 的回调函数的同步执行)
- 每个浅黄色框是Event Loop(事件循环)机制的一个阶段
- 每个阶段都有一个 FIFO(先入先出)的队列来执行回调
- 当Event Loop(事件循环)进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行
- 当该队列已用尽或达到回调限制,Event Loop(事件循环)将移动到下一阶段
阶段介绍:
timers(定时器):本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
待定回调:执行一些系统操作的回调,比如 TCP 错误
idle, prepare:仅系统内部使用
poll(轮询):
获取新的 I/O 事件;
执行与 I/O 相关的回调(几乎所有的 I/O 回调,如:操作读取文件等等。除了关闭的回调函数、计时器和 setImmediate() 回调),Node 将在适当的时候在此阻塞。
检测:setImmediate() 回调函数在这里执行
关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)。
6.2.2 Event Loop 可视化执行过程
事件循环执行可视化:

6.2.3 MacroTask(宏任务) 与 MicroTask(微任务)
Node 整体工作流程:

说明:
- Stack 是主线程调用栈,函数同步执行
- Node 遇到宏任务,会交给 Libuv 中的线程池(Background Threads)来执行它们,将执行完成后的回调放入 Task Queue
- Node 遇到微任务,会直接放入 Microtask Queue
- 当 Stack 中的函数执行完之后,开始运行事件循环 中的 MicroTask Queue,一直执行到 MicroTask Queue 清空为止,如果在执行 MicroTask Queue 中有新的 微任务产生,直接插入到 MicroTask Queue 后面
- 清空 MicroTask Queue 后开始执行 Task Queue 中的回调
Event Loop 具体处理过程:

微任务与 Event Loop 的关系:

详细说明:
Node
中一共有两个微任务,微任务不属于 Event Loop 任何阶段:
- process.nextTick()
- Promises
⚠️ 注意:process.nextTick() 的优先级比 Promises 高。
示例
setTimeout(() => {
console.log("timeout1");
Promise.resolve(1).then(() => {
console.log("Promise1");
});
process.nextTick(() => {
console.log("nextTick1");
});
});
setTimeout(() => {
console.log("timeout2");
Promise.resolve(1).then(() => {
console.log("Promise2");
});
});
// timeout1
// nextTick1
// Promise1
// timeout2
// Promise2
// nextTick1 早于 Promise1 打印出来,说明 nextTick 优先级更高。
- 我们可见的宏任务有 4 个:
- setTimeout()、setInterval()
- any I/O operation
- setImmediate()
- close Handlers
- 图中紫色的循环就是 Event Loop,任何阶段的宏任务执行完后都会先去执行微任务队列。
6.2.4 Event Loop 过程中的方法对比
setImmediate() VS setTimeout():
setImmediate() 是 Node 特有的方法,浏览器中没有。
它是一个在Event Loop(事件循环)的单独阶段运行的特殊计时器。
它使用一个 libuv API 来安排回调在 Event Loop 的 轮询(poll) 阶段完成后执行。
如下:
// timeout_vs_immediate.js
const fs = require("fs");
fs.readFile(__filename, () => {
setTimeout(() => {
console.log("timeout");
}, 0);
setImmediate(() => {
console.log("immediate");
});
});
// => immediate, timeout
在一个Event Loop(事件循环)中,setImmediate()永远比 setTimeout()先执行。
需要注意 ⚠️ 的是,如果是在主函数中,二者的执行顺序是不确定的。
如下,二者顺序无法确定。
setTimeout(() => {
console.log("timeout");
}, 0);
setImmediate(() => {
console.log("immediate");
});
process.nextTick() VS setImmediate() :
process.nextTick() 从技术上讲不是Event Loop(事件循环)的一部分。
nextTick()会把回调放入队列中,再下一个 Tick 时取出执行,所以,nextTick()回调在一个 Tick 中早于其他任务执行。
这是因为 nextTick() 属于 idle 观察者,setImmediate()属于 check 观察者。
在每一个轮询检查中,idle 观察者先于 I/O 观察者,I/O 观察者先于 check 观察者。
具体实现上,process.nextTick() 的回调保存在一个数组中,setImmediate()的结果保存在链表中。
process.nextTick()在每一轮循环中,将回调函数全部执行,而 setImmediate()在每轮中执行链表中的一个回调。
process.nextTick(function () {
console.log("nextTick 回调1");
});
process.nextTick(function () {
console.log("nextTick 回调2");
});
setImmediate(function () {
console.log("setImmediate 回调1");
process.nextTick(function () {
console.log("强势插入");
});
});
setImmediate(function () {
console.log("setImmediate 回调2");
});
console.log("同步执行");
// 同步执行
// nextTick 回调1
// nextTick 回调2
// setImmediate 回调1
// 强势插入 (因为一个Tick只执行一个 setImmediate)
// setImmediate 回调2
6.2.5 nextTick 的问题
process.nextTick() 有一个很大的问题,它会发生“饿死” I/O 的潜在风险:
fs.readFile("file.path", (err, file) => {});
const loopTick = () => {
process.nextTick(loopTick);
};
这段代码将会一直停留在 nextTick 阶段,无法进入到 fs.readFile 的回调中,这就是所谓的 I/O starving。
要解决这个问题,使用 setImmediate 替代,因为 setImmediate 属于事件循环,就算不停地循环,也不会阻塞整个事件循环机制。
Node 官网 Blog 中有这么一句话
我们建议开发人员在所有情况下都使用 setImmediate(),因为它更容易理解。
6.2.6 经典案例分析
示例分析:
console.log("script start");
const interval = setInterval(() => {
console.log("setInterval");
}, 0);
setTimeout(() => {
console.log("setTimeout 1");
Promise.resolve()
.then(() => console.log("promise 3"))
.then(() => console.log("promise 4"))
.then(() => {
setTimeout(() => {
console.log("setTimeout 2");
Promise.resolve()
.then(() => console.log("promise 5"))
.then(() => console.log("promise 6"))
.then(() => clearInterval(interval));
}, 0);
});
}, 0);
Promise.resolve()
.then(() => console.log("promise 1"))
.then(() => console.log("promise 2"));
Tick1:
执行 console.log("script start"),输出 => "script start";
Task Queue:【setInterval,setTimeout】;
Microtask Queue: 【() => console.log("promise 1"),() => console.log("promise 2")】;
当 Stack 执行完成后,开始执行 Microtask Queue 中的回调 输出 => "promise1", "promise2"
Task Queue:【setInterval,setTimeout】;
Tick2:
Microtask Queue 为空
执行 Task Queue 中的 setInterval,输出 => setInterval
在 setTimeout 1 之后调度另一个 setInterval
Task Queue:【setTimeout,setInterval】;
Tick3:
- Microtask Queue 为空
- 执行 Task Queue 中的 setTimeout
- 输出“setTimeout1”
- "Promise 3" 和 "Promise 4" 添加到 Microtask Queue
- 执行 Microtask Queue 中的 "Promise 3" 和 "Promise 4"
输出 => "Promise 3" 和 "Promise 4"
setTimeout2 添加到 Task Queue
Task Queue:【setInterval, setTimeout2】;
Tick4:
Microtask Queue 为空
执行 Task Queue 中的 setInterval
- 输出 => “setInterval”
添加新的 setInterval 到 Macrotask
Macrotask Queue: [setTimeout 2, setInterval]
Tick5:
Microtask Queue 为空
执行 setTimeout 2
输出 “setTimeout 2”
Microtask Queue: ["promise 5", "promise 6", clearInterval()]
执行 Microtask Queue 中的任务:
输出 => "promise 5"
输出 => "promise 6"
clearInterval(interval),取消 Macrotask Queue 中的 setInterval
Macrotask Queue: []
最终结果:
// script start
// promise 1
// promise 2
// setInterval
// setTimeout 1
// promise 3
// promise 4
// setInterval
// setTimeout 2
// promise 5
// promise 6
7. libuv
libuv
是 Node 的核心。
官方文档定义: libuv 是一个专注于异步 I / O 的多平台支持库。
uv 是什么意思?uv 是 Unicorn Velociraptor 的缩写,意思是“独角伶盗龙”,它是 libuv 的图标。
独角伶盗龙:

如果熟悉 C 语言的化可以看看libuv 源码。
每个操作系统都有自己的Event Demultiplexer(事件多路分解器)接口,🌟🌟🌟
- Linux -- epoll
- MacOS -- kqueue
- Windows -- IOCP
为了解决跨平台的问题,Node.js 官方团队提供了一个名为 libuv 的 C 库。
使 Node.js 与所有主要平台兼容,并规范不同类型资源的非阻塞行为。
libuv 不止提供了这样的底层抽象,还实现了 Reactor 模型,因此提供了一些列的 API,用于创建事件循环,管理事件队列,运行异步 I/O.
libuv 组成:

组成说明:
- 基于 epoll、kqueue、IOCP、event ports 实现的全能
事件循环
- 异步 TCP 和 UDP 套接字
- 异步 DNS 解析
- 异步文件和文件系统操作
- 文件系统事件
- ANSI 转义序列控制的 TTY
- IPC 经由套接字共享,使用 Unix 域套接字或命名管道(Windows)
- 子进程
- 线程池(多线程处理 I/O 操作回调)
- 信号处理
- 高清晰度时钟
- 线程和同步原语(primitive)
可以说 libuv 是和 V8 同样重要的 Node 核心。
更多 libuv 的内容可以看参考链接 4.
8. Node 的全局变量(global)
全局变量有:
Buffer 类
console
process
global(相当于浏览器的 window)
setImmediate()
setInterval()
setTimeout()
clearImmediate()
clearInterval()
clearTimeout()
V8 新增:
- WebAssembly
V10 新增:
- URL
- URLSearchParams
V11 新增:
- TextDecoder
- TextEncoder
- queueMicrotask(callback)
9. 模块系统(CommonJS)
在 Node.js 模块系统中,每个文件都被视为一个独立的模块。
9.1 缓存
模块在第一次加载后会被缓存。
多次调用 require(foo) 不会导致模块的代码被执行多次。
如果想要多次执行一个模块,可以导出一个函数,然后调用该函数。
模块是基于其解析的路径进行缓存的,所以只要文件或文件夹名称不同,就会多次重新加载,即使他们是同一个文件。
- 不同的文件名(比如从 node_modules 目录加载)不能保证 require('foo') 总能返回完全相同的对象
- ./Foo 和 ./foo 属于不同的文件夹
9.2 模块加载过程
Node 中引入模块主要过程:
- 路径分析
- 文件定位
- 编译执行
9.2.1 路径分析
加载文件模块:
- '/': 绝对路径加载模块
- './': 当前模块查找模块
- '../': 到上一个目录查找
找不到时会抛 code 属性为 'MODULE_NOT_FOUND' 的 Error。
加载 node_modules 目录模块:
如果传递给 require() 的模块标识符不是一个核心模块,也没有以 '/' 、 '../' 或 './' 开头。
Node.js 会从当前模块的父目录开始,尝试从它的 /node_modules 目录里加载模块。
如果还是没有找到,则移动到再上一层父目录,直到文件系统的根目录。
9.2.2 文件定位
从缓存加载时,无需路径分析、文件定位和编译执行。
文件定位主要有两个过程:
文件扩展名分析
Node 会按照 .js、.json、.node 的次序补足扩展名,依次尝试。
目录分析和包
Node 分析标识符时可能得到一个目录(如:require('./user')或引入一个包 require("axios")).
此时,Node 会查看目录的 package.json 文件中的 main属性指定的文件;
如果没有 package.json 会依次查找目录下的 index.js、index.json、index.node。
9.2.3 编译执行
定位到具体文件后,Node 会新建一个模块对象,然后根据路径载入并编译。
不同扩展名,载入方法不同:
- .js : fs 模块同步读取并编译
- .node : 这是 c/c++ 编写的扩展文件,通过 dlopen()方法载入并编译
- .json : fs 模块同步读取,然后 JSON.parse()解析返回结果
每一个编译成功的文件模块都会将其路径作为索引,缓存在 Module._catch 对象上。
JS 的编译过程:
Node 对获取的 JavaScript 文件内容进行了包装。
编译就是用下面的函数包裹住 JavaScript 代码:
(function (exports, require, module, __filename, __dirname) {
// 模块中的JS代码,如
var _ = require("lodash");
exports.flat = _.flat;
});
这样做的目的:
- 作用域隔离:保持了顶层的变量(用 var、 const 或 let 定义)作用在模块范围内,而不是全局对象
- 有助于提供一些看似全局的但实际上是模块特定的变量,如: module 、 exports 、 __filename 、 __dirname: 模块绝对文件名和目录路径
经过原生方法处理后,module.exports 属性被返回给了调用方。
这个过程就是 Node 对 CommonJS 模块规范的实现。
C/C++ 的编译过程:
Node 调用 process.dlopen()方法进行加载和执行。
dlopen()方法在 Windows 和 *nix 平台下分别有不同的实现,通过 libuv 兼容层进行了封装。
C/C++模块(.node 模块)其实不需要编译,只需要执行即可,执行过程中, module.exports 对象与 .node 模块产生联系,最后返回给调用者。
JSON 文件的编译过程:
- Node 利用 fs 模块同步读取 JSON 文件,调用 JSON.parse()解析文件,然后把它赋值, module.exports,供外部调用。
9.3 模块作用域
模块封装导致每个模块都有一个作用域。
作用域中的变量:
- __filename: 当前文件的文件名
console.log(__filename);
// 打印: /Users/mjr/example.js
console.log(__dirname);
// 打印: /Users/mjr
- __dirname : 当前模块目录名,相当于 __filename 的 path.dirname()。
console.log(__dirname);
// 打印: /Users/mjr
console.log(path.dirname(__filename));
// 打印: /Users/mjr
module: 对当前模块的引用。
module.exports: 用于指定一个模块所导出的内容,即可以通过 require() 访问的内容。 module.children: 被该模块引用的模块对象
require(id): 用于引入模块、 JSON、或本地文件
require.cache: 被引入的模块会缓存到这个对象中。谨慎使用! require.main: 表示当 Node.js 进程启动时加载的入口脚本的 Module 对象
exports: 对 module.exports 的一个引用,模块执行之前赋值给 module.exports。
module.exports.f = ... 可以更简洁地写成 exports.f = ...。
9.4 核心模块与文件模块
Node.js 有些核心模块
会被编译成二进制
。
JavaScript 核心模块定义在 Node.js 源代码的 lib/ 目录下;
C/C++ 核心模块定义在 Node.js 源代码的 src/ 目录下。
require() 总是会优先加载核心模块。
例如, require('http') 始终返回内置的 HTTP 模块,即使有同名文件。
文件模块是指我们编写的模块,包括 npm 安装的 node_modules 中的模块。
9.4.1 JavaScript 核心模块的编译过程
在编译 C/C++文件之前,编译程序会先将所有 JavaScript 模块文件编译为 C/C++代码。
JavaScript 核心模块也要经历包装过程。
与文件模块有区别的地方在于:获取源代码的方式(核型模块是从内存中加载的)以及缓存执行结果的位置。
与文件模块缓存到 Module._cache 对象上有所不同,编译成功的核心模块缓存到 NativeModule._cache 对象上。
9.4.2 C/C++ 核心模块的编译过程
C/C++ 模块主内完成核心,JavaScript 主外实现封装的模式是 Node 能够提高性能的常见方式。
Node 的 buffer、
crypto、
evals、
fs、
os
等模块都是部分通过 C/C++编写的。
C/C++ 编写的的部分统一称为 内建模块。
Node 模块的依赖关系:

一般情况下,文件模块可能依赖核心模块,核心模块会依赖内建模块。
内建模块是如何将内部变量或方法导出,以供外部 JavaScript 核心模块调用的呢 ❓
- Node 启动时,会生成全局变量 process,并提供 Binding() 方法类协助加载内建模块
- 加载内建模块时,先创建一个 exports 对象,然后调用 get_builtin_module() 方法取出内建加载模块对象,通过执行 register_func()方法填充 exports 对象
- 最后将 exports 对象按模块名缓存,并返回给调用方
9.4.2 核心模块的引入流程
如何让 C/C++ 模块符合 CommonJS 模块规范 ❓
整个过程相当复杂,以 os 核心模块为例,
os 模块从 C/C++ 到 JavaScript 过程:

9.5 C/C++扩展模块
作为一名前端开发者,曾经的一名工科大学生,在大一时学过一学期的 C 语言,然后就没有使用过了(当然,学的时候好像就没用过)😅 。
C/C++ 扩展模块会在出现性能瓶颈时对我们有很大帮助。
.node 文件的生成和平台有关,*nix 平台和 windows 平台下,需要经过不同的编译过程将 C/C++文件编译成 .node 文件,然后会被 Node 处理成 JavaScript 文件。
9.6 模块之间的调用关系
模块间的调用关系图:

说明:
- 是红色虚线是文件模块使用 process.binding() 方法直接调用 C/C++内建模块,不推荐
- 第三方编写的 C/C++ 扩展模块,为了提高性能,一般只由 JavaScript 文件模块调用
10. NPM
无论写不写 Node 项目,NPM 都已经是前端必备的工具了。
项目中由各种安装包(package),如 Vue、React、Webpack、Lodash 等。
包里面也有各自需要依赖的包。
包和 NPM 是将模块联系起来的一种机制。
Node 资源分享
汇集了很多 Node.js 优秀资源 -- awesome-nodejs
Node.js 中文社区 -- CNode 社区
参考
《深入浅出 Node.js》
《Node.js 设计模式 第二版》
C 大神可以仔细去研究 -- libuv 官网
官网文章,讨论如何处理线程,非常推荐 -- 不要阻塞你的事件循环(或是工作线程池)
官网文章,更容易理解阻塞与线程 -- 阻塞对比非阻塞一览
官网文章,思路清晰,有必要读 -- 一次 HTTP 传输解析
官方文章,事件循环 -- Node.js 事件循环,定时器和 process.nextTick()
醍醐灌顶的文章,讲 Event Loop -- Node.js Under The Hood #3 - Deep Dive Into the Event Loop
Event Loop 可视化,一目了然执行过程,作者太有心了JavaScript Visualized: Event Loop