读书笔记--深入浅出Node.js
Node.js 是一个基于 Chrome V8 引擎的 JavaScript __运行环境__。Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。
—— Node.js中文网
特点
异步 I/O
在Web端,过去大多数是同步的方式编写程序,这种串行调用下层应用数据的过程中充斥着串行的等待时间,如果采用多线程来解决这种串行等待,又或多或少地显得小题大做。在Node中,语言层面即可天然并行的特性在这种场景中显得十分有效。
例如读取文件。
var fs = require('fs');
fs.readFile('/etc/passwd1', (err, data) => {
if (err) throw err;
console.log('passwd1: ' + data);
});
fs.readFile('/etc/passwd2', (err, data) => {
if (err) throw err;
console.log('passwd2: ' + data);
});
对于同步I/O而言,它们的耗时是两个任务耗时之和,而对于异步I/O来说,耗时取决最慢的那个文件读取时间。
事件与回调函数
因为在 JavaScript 中,函数是一等公民,可以将函数作为对象传递给方法作为实参进行调用。
var http = require('http');
var querystring = require('querystring');
http
.createServer(function(req, res) {
var postData = '';
req.setEncoding('utf8');
req.on('data', function(trunk) {
postData += trunk;
});
req.on('end', function() {
res.end(postData);
});
})
.listen(8080);
console.log('服务器启动?成');
单线程
优点是不用像多线程那样处处在意共享状态的问题,没有死锁的存在,也没有线程上下文交换所带来的性能上的开销。
缺点是无法利用多核 CPU,错误会引起整个应用推出(应用的健壮性值得考验),大量计算占用 CPU 导致无法继续调用异步 I/O。
HTML5 定制了 Web Workers 的标准,Web Workers 能够创建工作线程来进行计算,以解决 JavaScript 大计算阻塞 UI 渲染的问题,工作线程为了不阻塞主线程,通过消息传递的方式来传递运行结果,这也使得工作线程不能访问到主线程中的 UI。
Node 采用了与 Web Workers 相同思路来解决单线程中大计算量的问题:child_process。子进程的出现,意味着 Node 可以从容地应对单线程在健壮性和无法利用多核 CPU 方面的问题。Node还可以通过编写C/C++扩展的方式更高效地利用CPU,将一些V8不能做到性能极致的地方通过C/C++实现。
跨平台
兼容Windows和*nix平台主要得益于Node在架构层面的改动,它在操作系统与Node上层模块系统之间构建了一层平台层架构,即libuv。
模块
模块分为 核心模块 和 文件模块 :
核心模块是 Node 提供的模块。在 Node 源代码的编译过程中,编译进了二进制执行文件。在 Node 进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的。
文件模块是用户编写的模块。在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。
在模块中,上下文提供require()
方法来引入外部模块,提供exports
对象用于导出变量或方法,还存在一个module
对象,它代表模块自身,而exports
是module
的属性,因此exports
只能以 exports.a = 1; exports.b = 2;
不能以 exports = { key: value }
的形式导出,通过 module.exports
则两种方式都可以。导出的同名属性或函数,后面的覆盖前面。
// math.js
exports.add = function() {
var sum = 0,
i = 0,
args = arguments,
l = args.length;
while (i < l) {
sum += args[i++];
}
return sum;
};
// index.js
var math = require('./math')
var result = math.add(2, 3)
console.log(result) // 5
模块加载
缓存
与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不通的地方在于,浏览器仅仅就缓存文件,而Node缓存的是编译和执行后的对象。
不论是核心模块还是文件模块, require()
方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的检查先于文件模块的缓存检查。
路径分析
require()
接收一个标识符作为参数,在Node实现中,正是基于这也一个标识符进行模块查找,主要分为以下几类:
-
http
、path
、file
这种核心模块。核心模块的优先级仅次于缓存加载,它在Node的源代码编译过程中已经编译为二进制代码,其加载速度最快。 -
.
、..
或者/
开始的路径文件模块。首次引入后将编译执行后的结果存放到缓存中。加载速度次于核心模块。 -
非路径形式的自定义文件模块。通过模块路径查找,这种方式是最耗时的。
模块路径是Node在定位文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组,与JavaScript的原型链或作用域链的查找方式十分相似。生成规则如下:
- 当前目录下的
node_modules
目录 - 父目录下的
node_modules
目录 - 父目录的父目录下的
node_modules
目录 - 逐级递归,直到根目录下的
node_modules
目录
- 当前目录下的
文件定位
在第一次引入文件模块的时候有下面两个细节需要注意:
- 文件扩展名分析
当没有指定扩展名,Node 按照.js
、.json
、.node
的次序尝试补足扩展名。在尝试过程中,需要调用fs
模块同步阻塞式的判断文件是否存在。因为Node是单线程的,所以这里是一个会引起性能问题的地方。所以除了.js
文件,都最好是加上文件扩展名。 - 目录分析和包
读取package.json
文件,通过main
属性指定的文件进行定位(如果文件名缺少扩展名,将会进入扩展名分析的步骤),如果main
属性指定的文件名错误,或者没有package.json
文件,将会依次查找index.js
、index.json
、index.node
。
编译执行
编译执行是引入文件模块的最后一个阶段。根据上面的步骤定位到具体文件后,Node会新建一个模块对象,然后根据路径载入并编译(对于不同的文件扩展名,其载入的方法也有所不同)。每一个编译成功的模块,都会将其文件路径作为索引(将编译执行后的结果作为值)缓存在Module._cache
对象上,以提高二次引入的性能。
-
.js
文件通过fs
模块同步读取文件后编译执行。 -
.json
文件通过fs
模块同步读取后,用JSON.parse()
解析返回结果。 -
.node
文件是用C/C++编写的扩展文件,通过dlopen()
方法加载最后编译生成的文件。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。