# 模块机制

# Node的模块实现

在Node中引入模块,需要经历如下3个步骤:

  • 路径分析
  • 文件定位
  • 编译执行

# 路径分析

require()方法接受一个标识符作为参数。模块标识符在Node中主要分为以下几类:

  • 核心模块,如http、fs。
  • .或..开始的相对路径文件模块
  • 以/开始的绝对路径文件模块
  • 非路径形式的文件模块,比如各种自定义模块。

核心模块的优先级仅次于缓存加载,它在Node的源代码编译过程中已经编译为二进制代码,其加载过程最快。如果试图加载一个与核心模块标识符相同的自定义模块,那是不会成功的,必须选择一个不同的标识符。

相对路径模块和绝对路径模块,在分析文件模块时,require()方法会将路径转为真实路径,并以真实路径作为索引,将编译执行的结果放在缓存中。

自定义模块会沿着模块路径逐级向上查找,查找每一级目录下的node_modules目录。自定义模块加载最慢

# 文件定位

require()在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。Node会按照.js、.json、.node的次序补足扩展名,依次尝试。在尝试过程中,需要调用fs模块同步阻塞地判断文件是否存在。因为Node是单线程的,所以这里是一个会引起性能问题的地方。

require()通过分析文件扩展名后,可能没有找到对应的文件,但是却得到了一个目录,此时Node会把目录当做一个包来处理。先会查找package.json文件,从中取出main属性指定的文件名进行定位。如果main属性指定的文件名错误,或者没有package.json文件,Node会把index当做默认文件名,然后依次查找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会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,具体载入的方法也不同:

  • js文件,通过fs模块同步读取文件后编译执行
  • node文件,这是用C/C++编写的扩展文件,通过dlopen()加载最后编译生成的文件
  • json文件,通过fs模块同步读取文件,用JSON.parse()解析返回结果

每一个编译成功的模块都会将其文件路径作为索引缓存在Modele._cache对象上,提高二次引入的性能。