第一章 Node 简介
异步IO
Node 底层构建了许多异步 IO 的 API,从文件读取到网络请求,这样的意义在于,我们可以从语言层面很自然地进行并行 IO 操作,每个调用之前无须等待之前的 IO 调用结束,极大提升效率。
事件驱动
Node 将前端浏览器中广泛且成熟的事件引入后端,配合异步 IO,将事件点暴露给业务逻辑。事件编程的方式具有轻量级,松耦合,只关注事务点等优势。
单线程
优点:单线程的最大好处是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁的存在,也没有线程上下文交换所带来性能上的开销。
缺点:像浏览器中 js 与 UI 共用一个线程,js 长期执行会导致 UI 的渲染和响应被中断。在 Node 中,长时间的 CPU 占用也会导致后续的异步 IO 发不出调用,已完成的异步 IO 回调函数得不到及时执行。
跨平台
Node 在 v0.6.0 版本支持了 windows 平台,这主要得益于 Node 架构层面的改动,它在操作系统与 Node 上层模块系统之间构建了一层平台层架构,即 libuv。
第二章 模块机制
基本概念
模块引用
var math = require('math');
模块定义:在 Node 中一个文件就是一个模块,将方法挂载在 exports 对象上作为属性即可定义导出的方式
// math.js
exports.add = function() {};
// program.js
var math = require('math');
exports.increment = function (val) {
return math.add(val, 1);
}
模块标识
模块标识其实就是传递给 require 方法的参数,必须符合小驼峰命名,可以是相对路径,绝对路径,可以没有后缀 .js
模块实现
在 Node 中引入模块,需要经历以下三个步骤:
- 路径分析
- 文件定位
- 编译执行
在 Node 中,模块又分为两类
- 核心模块:Node 提供的模块
- 文件模块:用户编写的模块
核心模块部分在 Node 源代码的编译过程中,编译进了二进制执行文件,在 Node 进程启动时,部分核心模块就被直接加载进内存中,省略掉了文件定位和编译执行的步骤,且在路径分析中优先判断,故加载最快。
文件模块则需要完整的路径分析,文件定位,编译执行,速度比核心模块慢。
缓存策略
与前端浏览器会缓存静态脚本文件提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,而 Node 缓存的是编译和执行之后的对象。
模块路径
模块标识符在 Node 中主要分为以下几类,核心模块,路径形式的文件模块,自定义模块,加载速度为:
缓存过的模块 > 核心模块 > 路径形式文件模块 > 自定义模块
原因是自定义模块 Node 在定位时使用模块路径的查找策略,具体表现为一个路径组成的数组。
[ '/home/usr/research/node_modules', '/home/usr/node_modules', '/node_modules']
它生成的规则如下:
- 当前文件目录下的 node_modules 目录
- 父目录下的 node_modules 目录
- 父目录的父目录下的 node_modules 目录
- 沿路径向上逐级递归,直到根目录下的 node_modules 目录
并且如果标识符不包含文件扩展名,Node 会按照 .js
,.json
,.node
的次序补足依次尝试。
模块编译
编译和执行是引入文件模块的最后一个阶段,对于不同的文件扩展名,载入方法也有所不同。
- .js 文件:通过 fs 同步读取后编译执行
- .node 文件:C/C++编写的扩展文件
- .json 文件:通过 fs 同步读取后,JSON.parse 解析
- 其余扩展名文件:均被当做 .js 文件载入
JavaScript 模块
在 CommonJS 规范中,每个模块文件都存在着 require
、exports
、modules
、__filename
、__dirname
,它们是从何而来呢?事实上,Node 对获取的 JavaScript 文件进行了头尾包装,一个正常的 JavaScript 文件会被包装成如下:
(function(exports, require, module, __filename, __dirname) {
// 模块代码实际存在于此处
});
在执行之后,模块的 exports 属性被返回给了调用方,exports 属性上的任何方法和属性都可以被外部调用到,但是模块中的其余变量或属性则不可直接被调用。
module.exports.f = ...
可以更简洁地写成 exports.f = ...
,相当于每个模块中存在一句var exports = module.exports;
,exports
对象是通过形参的方式传入的,如果直接赋值形参会改变形参的引用,等于切断了exports
与 module.exports
的联系。
exports.hello = true; // 从模块的 require 中导出
exports = { hello: false }; // 未导出,仅在模块中可用
C/C++ 模块
.node
的模块文件并不需要编译,因为他是 C/C++ 模块编译生成的,所以只有加载和执行的过程。Node 调用dlopen
方法进行加载和执行。
*nix 下通过 gcc/g++ 等编译器编译为动态链接共享对象文件(.so),在 window 下则需要通过 Visual C++ 的编译器编译为动态链接库库文件(.dll),.node 扩展名其实只是为了看起来自然一些,在 *nix 下它是一个 .so 文件,在 windows 下它是一个 .dll 文件。dlopen 方法内部实现时也区分了平台。
其实在应用中,可能会频繁的出现位运算的需求,包括转码,编码等过程。如果用 JavaScript 来实现,CPU 资源将会耗费很多(JavaScript 的位运算效率较低),此时 C/C++ 模块的优势就体现出来了,我们也可以自己编写 C++ 扩展模块来提升性能。
npm
CommonJS包规范是理论,NPM 是其中的一种实践。对于 Node 而言,NPM 帮助完成了第三方模块的发布,安装和依赖等。借助 NPM ,Node 与第三方模块之间形成了很好的生态系统。