主题
commonjs模块查找 和 加载机制
1. node中模块的分类
在Nodejs中,模块大致分为两类:
- 一类是Node本身提供的模块,称为核心模块
- 一类是用户编写的模块,称为文件模块
2. 模块加载步骤 及 快慢分析
一般情况下,require
引入模块的时候,需要经历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.js
、index.json
、index.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中的模块机制》,此处不再赘述