首页 关于我们

node模块加载

Word count: 1.1k / Reading time: 4 min
2016/04/10 Share

node的模块分为核心模块和文件模块,核心模块是node提供的内置模块,在node源码编译过程中,编译进了二进制执行文件,部分模块在node进程启动时就已经加载进内存中;而文件模块则是运行时动态加载。正如浏览器的缓存一样,node会对引入过的模块进行缓存,以减少二次引入的开销;不同的是node会缓存模块的结果对象而不只是文件。模块的加载包括三个步骤:

  1. 路径分析
  2. 文件定位
  3. 编译执行

路径分析

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中,每个文件模块都是一个对象它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
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呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)的形参修改不会影响实参的值,而数组、对象的形参修改会反映到实参上。这里涉及到的是按值传递和按共享传递。

  • 按值传递:函数的形参是被调用时所传实参的副本。修改形参的值并不会影响实参。
  • 按共享传递:对象是可变的,调用者和被调用者共享同一个对象,两者的修改都会互相可见。
CATALOG
  1. 1. 路径分析
  2. 2. 文件定位
  3. 3. 编译执行