老席老席杂货铺
Picture of github

CommonJS 模块

模块加载流程

路径分析

依据标识符确定模块文件位置(是内置模块还是文件模块,是相对路径还是node_modules内的模块)

  • path 策略:查找当前文件夹下面的node_modules,如果没找到,递归查找上层目录的node_modules,直到根目录,如果没找到就报错。可以在module.paths中查看对应的查找路径
  • 路径会作为模块的id,路径为绝对路径

文件定位

确定目标模块中具体的文件及文件类型

  • 先根据后缀匹配:.js →.json → .node,如果没有找到对应的文件,就认为是一个目录
  • 查找目录中的package.json 文件,通过JSON.parse() 解析package.json文件,拿到其main的属性,如果main的值也是没有后缀的,再根据上一步的规则进行匹配
  • 如果package.json 不存在或者main属性不存在,或者main指向的文件不存在,那nodejs会默认用目录中的index作为目标模块的具体文件名称,依次查找index.js → index.json → index.node
  • 如果index对应的文件也不存在,则根据路径分析的规则递归循环查找父目录,直到根目录

编译执行

采用对应的方式完成文件的编译执行

  • js文件的编译执行
    • 使用fs模块同步读取目标文件的内容
    • 对其内容进行语法包装,生成可执行js函数
    • 调用函数,传入exports, module, require, __dirname, __filename 等参数。(这就是为什么在nodejs中编写模块时可以使用上述参数。)
  • json文件的编译执行
    • 使用fs模块同步读取目标文件的内容
    • 将读取到的内容通过JSON.parse() 进行解析
    • 将解析结果返回给exports对象

缓存优化原则

加载过的模块会使用路径(也就是module.id 作为索引)缓存,一个模块多次加载,只会完整执行一次加载流程,后面再使用此模块会从缓存中获取,来提高加载速度。模块缓存时,会先根据id创建缓存对象,然后再编译执行,所以在循环引用时,如果先加载的模块a 中引用了b,就会暂停下来,先加载b, 这时a 模块导出的只是一个空对象,等b加载结束后,返回到a中继续执行,如果a代码中使用的是 module.exports = {}, 这时b就中的引用还是空对象,导致属性丢失。这就是使用module.exports的缺点。