CommonJs模块加载
测试用例
// a.js
console.log('a 模块执行 之前 ----')
module.exports.done = false
console.log('a 模块执行 导出之后 ----')
var b = require('./b.js')
console.log('a 模块执行 引入 b 模块之后 ----')
console.log(`a 模块中,b.done=${b.done} ---`)
module.exports.done = true
console.log('a 模块执行完毕 ------')
// b.js
console.log("b 模块开始执行了 module.exports 之前")
module.exports.done = false
console.log("b 模块开始执行了 module.exports 之后")
console.log("b 模块 加载 a 模块之前")
var a = require('./a.js')
console.log("b 文件 导入a模块之后")
console.log(`b 模块中,a.done =${a.done} a.xxx = ${a.xxx}`)
module.exports.done = true
console.log('b 模块执行完毕')
// node a.js
a 模块执行 之前 ----
a 模块执行 导出之后 ---- // 遇到require("./b.js") 加载b模块代码
b 模块开始执行了 module.exports 之前 // 执行b模块的代码
b 模块开始执行了 module.exports 之后
b 模块 加载 a 模块之前
b 文件 导入a模块之后 // 再次导入 a 模块,会先查找缓存是否存在,存在则直接返回exports的值
b 模块中,a.done =false a.xxx = xxxx
b 模块执行完毕 // b 模块执行完毕
a 模块执行 引入 b 模块之后 ----
a 模块中,b.done=true ---
a 模块执行完毕 ------
在 NodeJS 中有一个方法是我们使用频率最高的,那就是 require 方法。NodeJs 遵循 CommonJS 规范,该规范的核心是通过 require来加载其他依赖的模块。
- 获取node a.js 参数
require('internal/modules/``cjs``/loader').Module.runMain(process.argv[1]);
- 将入口文件作为 主模块运行
https://juejin.cn/post/6949385808755294245
几个问题
- module.exports 或者 exports 是全局变量吗?
- 模块的加载是同步还是异步?
- 循环引用会不会产生性能问题或者导致错误?
什么是 CommonJS
每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即module.exports
)是对外的接口。
Node 模块的分类
- build-in modules —— Nodejs 中以 C++ 形式提供的模块。
- constant module —— Nodejs 中定义常量的模块。
- native module —— Nodejs 中以 javascript 形式提供的模块。
- 第三方module —— 由第三方提供的模块。
module 对象
NodeJs 内部提供一个 Module 构建函数。所有模块都是 Module 的实例。
每个模块内部,都有一个 module 对象,代表当前模块。它有以下属性。
module 对象的属性
module.id
模块的识别符,通常是带有绝对路径的模块文件名。module.filename
模块的文件名,带有绝对路径。module.loaded
返回一个布尔值,表示模块是否已经完成加载。module.parent
返回一个对象,表示调用该模块的模块(程序入口文件的module.parent为null)module.children
返回一个数组,表示该模块要用到的其他模块。module.exports
表示模块对外输出的值。
module.exports 属性
module.exports
属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports
变量。module.exports
属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports
变量。exports 变量
我们有时候会这么写:
// test.js
function test(){console.log(test);
}
export.test = test;
// result.js
const test = require("./test")
这样也可以拿到正确的结果,这是因为:exports 变量指向 module.exports。这等同在每个模块头部,有一行这样的命令。
var exports = module.exports;
注意:不能直接给 exports 变量赋值,这样会改变 exports 的指向,不再指向 module.exports。在其他模块使用 require 方法是拿不到赋给 exports 的值的,因为 require 方法获取的是其他模块的 module.exports 的值。
建议:尽可能的使用 module.exports
来导出结果。
模块的流程
- 创建模块
- 导出模块
- 加载模块
- 使用模块
require 方法
require 是 node 用来加载并执行其它文件导出的模块的方法。 在 NodeJs 中,我们引入的任何一个模块都对应一个 Module 实例,包括入口文件。
完整步骤:
- 调用父模块的 require 方法(父模块是指调用模块的当前模块)
require = function require(path) {return mod.require(path);
}; //调用 Module 的 _load 方法
- 通过
Module._resolveFilename
获取模块的路径 fileName
const filename = Module._resolveFilename(request, parent, isMain);
- 根据 fileName 判断是否存在该模块的缓存
- 如果存在缓存,则调用
updateChildren
方法在更新缓存内容,并返回缓存 - 如果不存在缓存,则继续执行
- 当做原生模块,调用
loadNativeModule
方法进行加载
- 如果加载成功,则返回该原生模块
- 否则,继续执行
- 根据当前模块名(路径)和父模块对象生成一个 Module 实例:
const module = cachedModule || new Module(filename, parent);
- 再判断该模块是否是入口文件
if (isMain) {process.mainModule = module;module.id = '.';
}
- 将该模块的实例存入到 Module 的缓存中
Module._cache[filename] = module;
- 该模块的实例调用自身的
load
方法,根据 fileName 加载模块
module.load(filename);
- 获取该模块文件的后缀名称
const extension = findLongestRegisteredExtension(filename);
如果后缀名称是ES Module格式的(.mjs),则判断Module是否支持.mjs文件的解析,如果不支持,则抛出异常。
- 根据后缀名称解析模块文件内容
Module._extensions[extension](this, filename);
- 根据fileName读取文件内容
content = fs.readFileSync(filename, 'utf8');
- 编译并执行读取到的文件,调用 module 自身的
_complile
方法:
module._compile(content, filename);
_compile
主要内容步骤:
const compiledWrapper = wrapSafe(filename, content, this);
const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
let result;
const exports = this.exports;
const thisValue = exports;
const module = this;
result = compiledWrapper.call(thisValue, exports, require, module, filename, dirname);
return result;
wrapSafe
方法的返回值
具体获得上图结果的代码是:
const wrapper = Module.wrap(content);
return vm.runInThisContext(wrapper, {filename,lineOffset: 0,displayErrors: true,importModuleDynamically: async (specifier) => {const loader = asyncESM.ESMLoader;return loader.import(specifier, normalizeReferrerURL(filename));},
});
- 修改该模块的加载状态为true
this.loaded = true;
- 加载成功。
总结
通过上面的调试过程可得出以下结论:
- 在NodeJs中,从入口文件开始,一切皆 Module。
- 模块的加载是同步的。
- 由于缓存机制的存在,模块的循环引用对性能的影响微乎其微,并且循环引用到的模块可能是不完整的,并且可能会导致错
- require 查找模块的流程如下:
- 文件路径的解析流程图如下: