原理
一、认识
在Node
中引入模块,需要经历如下四个步骤:
- 路径分析
- 文件定位
- 编译执行
- 加入内存
二、路径分析
Node.js
中模块可以通过文件路径或名字获取模块的引用。模块的引用会映射到一个js
文件路径。 在Node
中模块分为两类:
-
一是Node提供的模块,称为核心模块(内置模块),内置模块公开了一些常用的
API
给开发者,并且它们在Node
进程开始的时候就预加载了 -
一类是用户编写的模块,称为文件模块。如通过
NPM
安装的第三方模块(third-party modules
)或本地模块(local modules
),每个模块都会暴露一个公开的API。以便开发者可以导入
执行后,Node
内部会载入内置模块或通过 NPM
安装的模块。require
函数会返回一个对象,该对象公开的API可能是函数、对象或者属性如函数、数组甚至任意类型的JS对象。
核心模块是Node
源码在编译过程中编译进了二进制执行文件。在Node
启动时这些模块就被加载进内存中,所以核心模块引入时省去了文件定位和编译执行两个步骤,并且在路径分析中优先判断,因此核心模块的加载速度是最快的。文件模块则是在运行时动态加载,速度比核心模块慢。
node
模块的载入及缓存机制
-
载入内置模块:
Node
的内置模块被编译为二进制形式,引用时直接使用名字而非文件路径。当第三方的模块和内置模块同名时,内置模块将覆盖第三方同名模块。因此命名时需要注意不要和内置模块同名。如获取一个http
模块 -
载入文件模块
-
载入文件目录模块: 可以直接
require
一个目录,假设有一个目录名为folder
,如const myMod = require('./folder')
此时,
Node
将搜索整个folder
目录,Node
会假设folder
为一个包并试图找到包定义文件package.json
。如果folder
目录里没有包含package.json
文件,Node
会假设默认主文件为index.js
,即会加载index.js
。如果index.js
也不存在, 那么加载将失败。 -
载入
node_modules
里的模块如果模块名不是路径,也不是内置模块,
Node
将试图去当前目录的node_modules
文件夹里搜索。如果当前目录的node_modules
里没有找到,Node
会从父目录的node_modules
里搜索,这样递归下去直到根目录。 -
自动缓存已载入模块: 对于已加载的模块
Node
会缓存下来,而不必每次都重新搜索 -
优先从缓存加载: 和浏览器会缓存静态
js
文件一样,Node
也会对引入的模块进行缓存,不同的是,浏览器仅仅缓存文件,而nodejs
缓存的是编译和执行后的对象(缓存内存)。require()
对相同模块的二次加载一律采用缓存优先的方式,这是第一优先级的,核心模块缓存检查先于文件模块的缓存检查。
三、文件定位
**
-
文件扩展名分析: 调用
require()
方法时若参数没有文件扩展名,Node
会按.js
、.json
、.node
的顺寻补足扩展名,依次尝试。在尝试过程中,需要调用
fs
模块阻塞式地判断文件是否存在。因为Node
的执行是单线程的,这是一个会引起性能问题的地方。如果是.node
或者·.json
·文件可以加上扩展名加快一点速度。另一个诀窍是: 同步配合缓存。 -
目录分析和包:
require()
分析文件扩展名后,可能没有查到对应文件,而是找到了一个目录,此时Node
会将目录当作一个包来处理。首先,
Node
在挡墙目录下查找package.json
,通过JSON.parse()
解析出包描述对象,从中取出main
属性指定的文件名进行定位。若main
属性指定文件名错误,或者没有pachage.json
文件,Node
会将index
当作默认文件名。简而言之,如果
require
绝对路径的文件,查找时不会去遍历每一个node_modules
目录,其速度最快。其余流程如下:- 从
module path
数组中取出第一个目录作为查找基准 - 直接从目录中查找该文件,如果存在,则结束查找。如果不存在,则进行下一条查找
- 尝试添加
.js
、.json
、.node
后缀后查找,如果存在文件,则结束查找。如果不存在,则进行下一条 - 尝试将
require
的参数作为一个包来进行查找,读取目录下的package.json
文件,取得main
参数指定的文件 - 尝试查找该文件,如果存在,则结束查找。如果不存在,则进行第3条查找
- 如果继续失败,则取出
module path
数组中的下一个目录作为基准查找,循环第1至5个步骤 - 如果继续失败,循环第1至6个步骤,直到
module path
中的最后一个 - 如果仍然失败,则抛出异常
整个查找过程十分类似原型链的查找和作用域的查找。所幸
Node.js
对路径查找实现了缓存机制,否则由于每次判断路径都是同步阻塞式进行,会导致严重的性能消耗。一旦加载成功就以模块的路径进行缓存,加载过程如图所示:Preview - 从
四、编译执行
每个模块文件模块都是一个对象,它的定义如下:
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if(parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
对于不同扩展名,其载入方法也有所不同:
-
.js
通过fs
模块同步读取文件后编译执行: 在编译的过程中,Node
对获取的javascript
文件内容进行了头尾包装,将文件内容包装在一个function
中:(function (exports, require, module, __filename, __dirname) {
var math = require(‘math‘);
exports.area = function(radius) {
return Math.PI * radius * radius;
}
})包装之后的代码会通过
vm
原生模块的runInThisContext()
方法执行(具有明确上下文,不污染全局),返回一个具体的function
对象,最后传参执行,执行后返回module.exports
. -
.node
这是C/C++
编写的扩展文件,通过dlopen()
方法加载最后编译生成的文件: 核心模块分为C/C++
编写和JavaScript
编写的两个部分,其中C/C++
文件放在Node
项目的src
目录下,JavaScript
文件放在lib
目录下。-
转存为C/C++代码:
Node
采用了V8
附带的js2c.py
工具,将所有内置的JavaScript
代码转换成C++
里的数组,生成node_natives.h
头文件:namespace node {
const char node_native[] = { 47, 47, ..};
const char dgram_native[] = { 47, 47, ..};
const char console_native = { 47, 47, ..};
const char buffer_native = { 47, 47, ..};
const char querystring_native = { 47, 47, ..};
const char punycode_native = { 47, 47, ..};
...
struct _native {
const char* name;
const char* source;
size_t source_len;
}
static const struct _native natives[] = {
{ "node", node_native, sizeof(node_native)-1},
{ "dgram", dgram_native, sizeof(dgram_native)-1},
...
};
}在这个过程中,
JavaScript
代码以字符串形式存储在node
命名空间中,是不可直接执行的。在启动Node
进程时,js
代码直接加载到内存中。在加载的过程中,js
核心模块经历标识符分析后直接定位到内存中。 -
编译js核心模块:
lib
目录下的模块文件也在引入过程中经历了头尾包装的过程,然后才执行和导出了exports
对象。与文件模块的区别在于: 获取源代码的方式(核心模块从内存加载)和缓存执行结果的位置。js
核心模块源文件通过process.binding('natives')
取出,编译成功的模块缓存到NativeModule._cache
上。代码如下:function NativeModule() {
this.filename = id + '.js';
this.id = id;
this.exports = {};
this.loaded = fales;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};
-
-
.json
同过fs
模块同步读取文件后,用JSON.pares()
解析返回结果:.json
文件调用的方法如下:其实就是调用JSON.parse
//Native extension for .json
Module._extensions['.json'] = function(module, filename) {
var content = NativeModule.require('fs').readFileSync(filename, 'utf-8');
try {
module.exports = JSON.parse(stripBOM(content));
} catch(err) {
err.message = filename + ':' + err.message;
throw err;
}
}Module._extensions
会被赋值给require()
的extensions
属性,所以可以用:console.log(require.extensions);
输出系统中已有的扩展加载方式。 当然也可以自己增加一些特殊的加载:require.extensions['.txt'] = function(){
//code
};。但是官方不鼓励通过这种方式自定义扩展名加载,而是期望先将其他语言或文件编译成
JavaScript
文件后再加载,这样的好处在于不讲烦琐的编译加载等过程引入Node
的执行过程。 -
其他当作
.js
每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache
对象上。
五、加入内存
六、Require 原理
6.1 require() 加载顺序
-
如果
X
是内置模块(比如require('http')
)- 返回该模块
- 不再继续执行
-
如果 X 以 "./" 或者 "/" 或者 "../" 开头
-
根据 X 所在的父模块,确定 X 的绝对路径
-
将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
X
X.js
X.json
X.node -
将
X
当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行X/package.json(main字段)
X/index.js
X/index.json
X/index.node
-
-
如果
X
不带路径- 根据 X 所在的父模块,确定 X 可能的安装目录
- 依次在每个目录中,将 X 当成文件名或目录名加载
-
抛出 "not found"
6.2 require 相关源码
require
的源码在 Node
的 lib/module.js
文件
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
this.filename = null;
this.loaded = false;
this.children = [];
}
module.exports = Module;
var module = new Module(filename, parent);
上面代码中,Node
定义了一个构造函数 Module
,所有的模块都是 Module
的实例。可以看到,当前模块(module.js
)也是 Module
的一个实例。
每个实例都有自己的属性。下面通过一个例子,看看这些属性的值是什么。新建一个脚本文件 a.js
.
// a.js
console.log('module.id: ', module.id);
console.log('module.exports: ', module.exports);
console.log('module.parent: ', module.parent);
console.log('module.filename: ', module.filename);
console.log('module.loaded: ', module.loaded);
console.log('module.children: ', module.children);
console.log('module.paths: ', module.paths);
运行这个脚本
$ node a.js
module.id: .
module.exports: {}
module.parent: null
module.filename: /home/ruanyf/tmp/a.js
module.loaded: false
module.children: []
module.paths: [ '/home/ruanyf/tmp/node_modules',
'/home/ruanyf/node_modules',
'/home/node_modules',
'/node_modules' ]
可以看到,如果没有父模块,直接调用当前模块,parent
属性就是 null
,id
属性就是一个点。filename
属性是模块的绝对路径,path
属性是一个数组,包含了模块可能的位置。另外,输出这些内容时,模块还没有全部加载,所以 loaded
属性为 false
。
每个模块实例都有一个 require
方法
Module.prototype.require = function(path) {
return Module._load(path, this);
};
由此可知,require
并不是全局性命令,而是每个模块提供的一个内部方法,也就是说,只有在模块内部才能使用 require
命令(唯一的例外是 REPL
环境)。另外,require
其实内部调用 Module._load
方法。
下面来看 Module._load
的源码
Module._load = function(request, parent, isMain) {
// 计算绝对路径
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
// 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
// 第三步:生成模块实例,存入缓存
var module = new Module(filename, parent);
Module._cache[filename] = module;
// 第四步:加载模块
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
return module.exports;
};
上面代码中,首先解析出模块的绝对路径(filename
),以它作为模块的识别符。然后,如果模块已经在缓存中,就从缓存取出;如果不在缓存中,就加载模块。
因此,Module._load
的关键步骤是两个
Module._resolveFilename() :确定模块的绝对路径
module.load():加载模块
-
模块的绝对路径
下面是
Module._resolveFilename
方法的源码Module._resolveFilename = function(request, parent) {
// 第一步:如果是内置模块,不含路径返回
if (NativeModule.exists(request)) {
return request;
}
// 第二步:确定所有可能的路径
var resolvedModule = Module._resolveLookupPaths(request, parent);
var id = resolvedModule[0];
var paths = resolvedModule[1];
// 第三步:确定哪一个路径为真
var filename = Module._findPath(request, paths);
if (!filename) {
var err = new Error("Cannot find module '" + request + "'");
err.code = 'MODULE_NOT_FOUND';
throw err;
}
return filename;
};上面代码中,在
Module.resolveFilename
方法内部,又调用了两个方法Module.resolveLookupPaths()
和Module._findPath()
,前者用来列出可能的路径,后者用来确认哪一个路径为真。为了简洁起见,这里只给出
Module._resolveLookupPaths()
的运行结果。[ '/home/ruanyf/tmp/node_modules',
'/home/ruanyf/node_modules',
'/home/node_modules',
'/node_modules'
'/home/ruanyf/.node_modules',
'/home/ruanyf/.node_libraries',
'$Prefix/lib/node' ]上面的数组,就是模块所有可能的路径。基本上是,从当前路径开始一级级向上寻找
node_modules
子目录。最后那三个路径,主要是为了历史原因保持兼容,实际上已经很少用了有了可能的路径以后,下面就是
Module._findPath()
的源码,用来确定到底哪一个是正确路径。Module._findPath = function(request, paths) {
// 列出所有可能的后缀名:.js,.json, .node
var exts = Object.keys(Module._extensions);
// 如果是绝对路径,就不再搜索
if (request.charAt(0) === '/') {
paths = [''];
}
// 是否有后缀的目录斜杠
var trailingSlash = (request.slice(-1) === '/');
// 第一步:如果当前路径已在缓存中,就直接返回缓存
var cacheKey = JSON.stringify({request: request, paths: paths});
if (Module._pathCache[cacheKey]) {
return Module._pathCache[cacheKey];
}
// 第二步:依次遍历所有路径
for (var i = 0, PL = paths.length; i < PL; i++) {
var basePath = path.resolve(paths[i], request);
var filename;
if (!trailingSlash) {
// 第三步:是否存在该模块文件
filename = tryFile(basePath);
if (!filename && !trailingSlash) {
// 第四步:该模块文件加上后缀名,是否存在
filename = tryExtensions(basePath, exts);
}
}
// 第五步:目录中是否存在 package.json
if (!filename) {
filename = tryPackage(basePath, exts);
}
if (!filename) {
// 第六步:是否存在目录名 + index + 后缀名
filename = tryExtensions(path.resolve(basePath, 'index'), exts);
}
// 第七步:将找到的文件路径存入返回缓存,然后返回
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
}
// 第八步:没有找到文件,返回false
return false;
};经过上面代码,就可以找到模块的绝对路径了。
有时在项目代码中,需要调用模块的绝对路径,那么除了
module.filename
,Node
还提供一个require.resolve
方法,供外部调用,用于从模块名取到绝对路径。require.resolve = function(request) {
return Module._resolveFilename(request, self);
};
// 用法
require.resolve('a.js')
// 返回 /home/ruanyf/tmp/a.js -
加载模块
有了模块的绝对路径,就可以加载该模块了。下面是
module.load
方法的源码。Module.prototype.load = function(filename) {
var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
this.loaded = true;
};上面代码中,首先确定模块的后缀名,不同的后缀名对应不同的加载方法。下面是
.js
和.json
后缀名对应的处理方法。Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(stripBOM(content), filename);
};
Module._extensions['.json'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
try {
module.exports = JSON.parse(stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};