FAQ
同步 require()
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(internalModule.stripBOM(content), filename);
};
大家可能都有疑问:为什么会选择使用同步而不用异步实现呢?
之所以同步是 Node.js 所遵循的 CommonJS 的模块规范要求的, 具体来说
在当年,CommonJS 社区对此就有很多争议,导致了坚持异步的 AMD 从 CommonJS 中分裂出来。
CommonJS 模块是同步加载和同步执行,AMD 模块是异步加载和异步执行,CMD(Sea.js)模块是异步加载和同步执行。ES6 的模块体系最后选择的是异步加载和同步执行。也就是 Sea.js 的行为是最接近 ES6 模块的。不过 Sea.js 这样做是需要付出代价的——需要扫描代码提取依赖,所以它不像 CommonJS/AMD 是纯运行时的模块系统。
注意 Sea.js 是 2010年之后开发的,提出 CMD 更晚。Node.js 当年(2009年)只有 CommonJS 和 AMD 两个选择。就算当时已经有 CMD 的等价提案,从性能角度出发,Node.js 不太可能选择需要静态分析开销的 类 CMD 方案。考虑到 Node.js 的模块是来自于本地文件系统,最后 Node.js 选择了看上去更简单的 CommonJS 模块规范,直到今天。
从模块规范的角度来看,依赖的同步获取是几乎所有模块机制的首选,是符合由无数的语言奠定的开发者的直觉。
从模块本身的特性来说的,结论就是使用异步的require收益很小,同时对开发者并不友好。
fs.realpath 缓存
如今的 realpath
的实现变得非常简洁, 直接调用系统调用realpath。
fs.realpath = function realpath(path, options, callback) {
if (!options) {
options = {};
} else if (typeof options === 'function') {
callback = options;
options = {};
} else if (typeof options === 'string') {
options = {encoding: options};
} else if (typeof options !== 'object') {
throw new TypeError('"options" must be a string or an object');
}
callback = makeCallback(callback);
if (!nullCheck(path, callback))
return;
var req = new FSReqWrap();
req.oncomplete = callback;
binding.realpath(pathModule._makeLong(path), options.encoding, req);
return;
};
大家可能又有疑问了, 原本提升性能的路径缓存去哪里了,不是说缓存都是提升性能的重要手段吗?
社区的修改可以在 https://github.com/nodejs/node/pull/3594 看到,
fs: optimize realpath using uv_fs_realpath()
Remove realpath() and realpathSync() cache. Use the native uv_fs_realpath() which is faster then the JS implementation by a few orders of magnitude
去掉了缓存反而提升了性能, 作者的 commit 提交也写的非常清楚:native uv_fs_realpath 实现要大大优于js层的实现, 但并没有说具体原因。
前面我已经提到过了文件系统的基本原理和大致实现,VFS中引入了高速磁盘缓存的机制,这属于一种软件机制,允许内核将原本存在磁盘上的某些信息保存在RAM中,以便对这些数据的进一步访问能快速进行,而不必慢速访问磁盘本身。 高速磁盘缓存可大致分为以下三种:
- 目录项高速缓存——主要存放的是描述文件系统路径名的目录项对象
- 索引节点高速缓存——主要存放的是描述磁盘索引节点的索引节点对象
- 页高速缓存——主要存放的是完整的数据页对象,每个页所包含的数据一定属于某个文件,同时,所有的文件读写操作都依赖于页高速缓存。其是Linux内核所使用的主要磁盘高速缓存。
readpath
的 native 实现的高性能得益于目录项高速缓存,有自身的淘汰机制,保持自身的高效的访问。其实缓存机制依然存在,只是下移到 VFS文件系统层面了。
流式读
nodejs的fs模块并没有提供一个copy的方法,但我们可以很容易的实现一个,比如:
var source = fs.readFileSync('/path/to/source', {encoding: 'utf8'});
fs.writeFileSync('/path/to/dest', source);
这种方式是把文件内容全部读入内存,然后再写入文件,对于小型的文本文件,这没有多大问题。但是对于体积较大的二进制文件,比如音频、视频文件,动辄几个GB大小,如果使用这种方法,很容易使内存“爆仓”。具体的说,对于32位系统是1GB,64位是2GB。
理想的方法应该是读一部分,写一部分,不管文件有多大,只要时间允许,总会处理完成,这里就需要用到流的概念。
上面的文件复制可以简单实现一下:
// pipe自动调用了data,end等事件
fs.createReadStream('/path/to/source').pipe(fs.createWriteStream('/path/to/dest'));
源文件通过管道自动流向了目标文件。
总结
不要迷信异步, 使用时评估同步和异步的开销,包括复杂度和性能。
缓存策略需要综合考虑,这离不开对系统的了解(更多的涉猎),重复缓存只会带来没必要的开销。
大文件的操作,使用流式操作。