Skip to content

commonjs模块查找 和 加载机制

1. node中模块的分类

在Nodejs中,模块大致分为两类:

  • 一类是Node本身提供的模块,称为核心模块
  • 一类是用户编写的模块,称为文件模块

2. 模块加载步骤 及 快慢分析

一般情况下,require引入模块的时候,需要经历3步:

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

核心模块部分在Node源代码的编译过程中,编译进了二进制文件中,在Node进程启动时,部分核心模块就被加载进了内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分子时候优先判断。所以它的加载速度是最快的。

文件模块则是在运行时候动态加载,需要完整的路径分析,文件定位,编译执行过程,速度比核心模块慢。

因此:就加载速度来说:核心模块加载速度 > 文件模块(非核心模块)加载速度

3. 优先从缓存加载

缓存使得二次加载模块时候省去了路径分析文件定位编译执行的步骤,直接返回编译后的结果,提高了加载速度

前端浏览器会缓存静态脚文件提高性能,同样,Nodejs对引入过的模块会进行缓存,以减少二次引入时的开销,不同的地方在于,浏览器仅仅缓存文件,而Node缓存的是编译执行之后的对象

不论是核心模块还是文件模块,require()方法对相同的模块的二次加载都一律采用缓存优先的方式,这是第一优先级的,不同之处在于,核心模块的缓存检查先于文件模块的缓存检查

在《实现commonjs中的模块机制》一文中可以知道,若是模块首次加载,才会执行路径分析,文件定位,编译执行,并将执行后的结果返回。这时候,也会将编译执行后的结果存入缓存中一份,以完成的绝对路径做为唯一标识,再次加载这个模块的时候,先进行路径分析,然后就直接返回缓存中路径匹配的结果,从而节省了开销。提高加载速度。

4. 路径分析

require()方法接受一个标识符,对于不同标识符,模块查找和定位有不同程度的差异。

Node中,模块标识符主要分为以下几类:

  • 核心模块:例如 fs、http、path等
  • ...开始的相对路径文件模块
  • /开头的绝对路径文件模块
  • 非路径形式的文件模块,例如moment模块

4.1 核心模块加载

由于核心模块是在Node源码编译过程中就被变异成二进制代码了,在Node进程启动后已经存在内存中,所以,核心模块的优先级仅次于缓存加载,加载速度也是仅次于缓存加载, 核心模块再次加载也会走缓存。

注意: 若是自己编写的本地模块的名称与核心模块名称重名了,通过require('XXX')的方式是无法进行加载的,除非加上路径,例如:require('./xxx')这样才行

4.2 路径形式的文件模块加载

由于文件模块给noderequire()方法指明了确切文件位置,所以在查找过程中可以节约大量时间,但其加载速度慢于核心模块。

4.3 自定义模块(包)加载

第三方包的加载是最耗时间的,包括以require('a')的方式加载本地模块。包的加载其实就是一种特殊的文件模块,这种方式的加载速度是所有方式中最慢的一种。

require('xxx')引入一个第三方包,首先进行路径分析,路径分析时候,就会有一个路径查找策略生成规则,规则如下: ① 查找当前目录下的node_modules目录 ② 查找父级目录下的node_modules目录 ③ 查找父级的父级目录下的node_modules目录 ④ 沿路径向上逐级递归,直到根目录下的node_modules目录(这里的根目录指的的计算机根目录)

我们可以在一个模块内输出console.log(module.paths),可以得到下面的数组:

sh
// Linux下可能输出下面的数组:
['/home/serve/node/node_modules',
'/home/serve/node_modules',
'/home/node_modules',
'/node_modules',
]

// windows下可能得到下面的数组:
['c:\\nodejs\\node_modules','c:\\node_modules']

在加载过程中,node会逐个尝试模块路径中的路径,直到找到目标文件为止,所以,路径越深,模块查找会耗时越多,这就是自定义模块加载是最慢的原因。

5. 文件定位

5.1 文件扩展名分析

我们在使用commonjs规范require()函数时,允许传入的参数不带扩展名,这种情况,Node会按照.js.json.node的顺序补上扩展名后尝试。 在尝试过程中,调用的是fs.existsSync同步阻塞的方法判断文件是否真的存在,因此这里可能会有一定的性能问题。

技巧: 1. 若文件不是js文件,建议带上扩展名,可以提高一点加载速度。 2. 同步配和缓存

5.2 目录分析和包

在分析路径的过程中, 可能没有得到文件路径,但是得到一个同名的文件夹的路径,Nodejs模块化对CommonJS规范进行了一定程度的支持。所以,目录分析流程如下: ① Node在当前目录下查找package.json文件,通过JSON.parse解析这个文件,得到main字段值 ② 有main字段,按照main字段的值进路径分析,(包括添加扩展名) ③ 若main字段的路径错误,或者根本没有package.json文件,Node会根据当前目录下的index文件当做默认文件名,一次查找index.jsindex.jsonindex.node文件 ④ 若在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找, ⑤ 若模块路径都被便利完成,依然没有查找到目标文件,则会抛出查找失败的异常

6. 编译执行

编译和执行是引入文件模块的最后一个阶段,定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译,对于不同扩展名,载入的方式不同,具体如下:

  • .js文件:通过fs模块同步读取文件后编译执行
  • .json文件:通过fs同步读取文件后,用JSON.parse()解析返回结果
  • .node文件:这是用c/c++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件
  • 其余扩展名文件:他们都被当最js文件载入

这里说明一点:实际上,扩展名为.node的模块文件其实不需要编译,因为他是编写c/c++模块之后编译生成的,所以这里只有加载和执行的过程。在执行过程中,模块的exports对象与.node模块产生联系,然后返回给调用者

node调用process.dlopen()方法进行c/c++模块的加载和执行,在Node中,dlopen方法在windows和*nux平台下分别有不同实现,通过libuv兼容层进行了封装。

js与json具体的解析方式可以查看《实现commonjs中的模块机制》,此处不再赘述