node模块加载

Authors
  • avatar
    Name
    小明&小艺
    Twitter

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

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)的形参修改不会影响实参的值,而数组、对象的形参修改会反映到实参上。这里涉及到的是按值传递和按共享传递。

  • 按值传递:函数的形参是被调用时所传实参的副本。修改形参的值并不会影响实参。
  • 按共享传递:对象是可变的,调用者和被调用者共享同一个对象,两者的修改都会互相可见。