node模块加载
- Authors
- Name
- 小明&小艺
node 的模块分为核心模块和文件模块,核心模块是 node 提供的内置模块,在 node 源码编译过程中,编译进了二进制执行文件,部分模块在 node 进程启动时就已经加载进内存中;而文件模块则是运行时动态加载。正如浏览器的缓存一样,node 会对引入过的模块进行缓存,以减少二次引入的开销;不同的是 node 会缓存模块的结果对象而不只是文件。模块的加载包括三个步骤:
- 路径分析
- 文件定位
- 编译执行
路径分析
node 的 require 方法接受一个标识符作参数,实现模块加载,而该标识符主要有以下几类:
- 核心模块,如 http、fs、path 等
- .或者..开头的相对路径文件模块
- 以/开始的绝对路径文件模块
- 非路径形式的文件模块,如第三方插件
文件定位
核心模块
核心模块是优先加载的,也因为这个,与核心模块同名的第三方模块将不会加载。
路径形式的文件模块
以.或者..或者/开始的标识符,都会当做文件模块处理,会先将其转化为真实的路径,并以真实的路径作为索引,缓存到内存中,以便第二次加载。
第三方模块
这类模块可能是一个文件也可能是一个包,加载比较耗时。其查找文件的方式如下:
- 在当前文件目录下的 node_modules 目录
- 父目录下的 node_modules 目录
- 父目录的父目录的 node_modules 目录 ···
- 直到根目录的 node_modules 目录
对于不包含后缀名的标识,node 会按.js、.json、.node 的次序尝试查找文件,在尝试过程中是同步的,所以写的时候如果带上后缀会速度快一点。
如果 node 找到一个和标识符一样的文件夹,node 会当一个包来处理,查找目录下的 package.json 文件,通过 JSON.parse 解析文件的内容,从中找出 main 属性指定的文件名进行定位,如果仍没有文件则会找默认的 index.js、index.json、index.node。
编译执行
在 node 中,每个文件模块都是一个对象它的定义如下:
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 = [];
}
在编译和执行的时候,node 会按上面构建一个对象,再把找到的模块文件载入到对象中。对于不同的文件扩展名,node 会用不同的方式载入:
- .js 文件。通过 fs 模块读取后编译执行
- .node 文件。是用 c 或者 c++编写的扩展文件,通过 dlopen()方法加载后编译生成的文件
- .json 文件。通过 fs 读取后用 JSON.parse()解释返回结果
- 其余扩展名文件。被当成.js 文件载入
每次模块载入之后,都会将其文件路径作为索引缓存到 Module.cache 对象上,以提高二次引入性能。
在载入模块文件过程中,为了避免局部变量污染,会对 js 文件进行头尾包装,在头部添加(function (exports, require, module, **filename, **dirname) ;在尾部添加)。这样每个文件之间就进行了作用域隔离,包装后的文件会通过 vm 原生模块的 runInThisContext(类似 eval,只是具有具体的上下文,不会污染全局)返回一个 function,而其 exports 会返回给调用方。
需要注意的是:模块里既然有 exports 可以导出接口,为何需要 module.exports 呢
function test(module, exports){
module.exports = 100;
exports = 100;
}
var module = {exports: 10}, exports = 10;
console.log("before->module.exports:", module.exports);
console.log("before->exports:", exports);
test(module, exports);
console.log("after->module.exports:", module.exports);
console.log("after->exports:", exports);
//输出如下
//before->module.exports: 10
//before->exports: 10
//after->module.exports: 100
//after->exports: 10
以上的原因是:基础数据类型(number、string、boolean)的形参修改不会影响实参的值,而数组、对象的形参修改会反映到实参上。这里涉及到的是按值传递和按共享传递。
- 按值传递:函数的形参是被调用时所传实参的副本。修改形参的值并不会影响实参。
- 按共享传递:对象是可变的,调用者和被调用者共享同一个对象,两者的修改都会互相可见。