模块基本数据结构

其中children中的Module表示正常模块,当出现Cycular时表示是循环依赖的模块。
源码中Module定义module:
function Module(id = '', parent) {this.id = id;this.path = paht.dirname(id);this.exports = {};this.parent = parent;updateChildren(parent, this, false);this.filename = null;this.loaded = false;this.children = []}
module的相关属性会在load和compile时赋值。
核心模块
核心模块即内置在Node源码中的相关模块,统一存放在lib/目录下,而且核心模块只需要通过其标识符即可require(),如require(‘http’)会返回内置的HTTP模块。
模块解析过程
Module._load:
检查是否缓存过,有缓存则直接返回缓存内容;否则判断是否原生模块,是则调用NativeModule.require()方法并返回结果;
否则创建一个新的模块new Module()并保存到缓存中,加载文件内容并解析
Module._compile:
该函数负责解析模块的文件内容,其实是在指定的上下文环境中执行文件内容,并对其进行包装
- Resolution: 确定文件路径
- Loading: 确定文件类型
- Wrapping: 定义局部作用域
- Evaluation: VM执行文件内容
- Caching: 缓存文件内容
Loading时会根据文件类型不同确定对应的加载函数:
// Native extension for .js// .js文件加载过程Module._extensions[‘.js’] = function(module, filename) {if (filename.endsWith('.js')) {const pkg = readPackageScope(filename);// Function require shouldn’t be used in ES modules.if (pkg && pkg.data && pkg.data.type === ‘module’) {const parentPath = module.parent && module.parent.filename;const packageJsonPath = path.resolve(pkg.path, ‘package.json’);throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);}}// 读取文件内容并执行”编译”const content = fs.readFileSync(filename, ‘utf8’);module._compile(content, filename);};// Native extension for .jsonModule._extensions[‘.json’] = function(module, filename) {const content = fs.readFileSync(filename, ‘utf8’);if (manifest) {const moduleURL = pathToFileURL(filename);manifest.assertIntegrity(moduleURL, content);}try {module.exports = JSONParse(stripBOM(content));} catch (err) {err.message = filename + ‘: ‘ + err.message;throw err;}};// Native extension for .nodeModule._extensions[‘.node’] = function(module, filename) {if (manifest) {const content = fs.readFileSync(filename);const moduleURL = pathToFileURL(filename);manifest.assertIntegrity(moduleURL, content);}// Be aware this doesn’t use `content`return process.dlopen(module, path.toNamespacedPath(filename));};
在模块真正执行前,都会被包装一个外部函数,如下:
(function(exports, require, module, __filename, __dirname) {
// module scripts
});
可以在REPL模式下执行require('module').wrapper来查看具体的包装函数:
> require('module').wrapper
Proxy [
[
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
],
{ set: [Function: set], defineProperty: [Function: defineProperty] }
]
可见其实是一个数组,具体包装过程就是将模块内容插入到中间,最终返回数据拼接后的内容。
let wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1]
}
这样做首先可以保证变量定义是模块内局部的,不会跑到全局对象;其次,module和exports可以用来对外提供导出值,而__filename和__dirname则是module的绝对文件名和目录路径。
另外注意区分:exports仅是module.exports的一个引用,真正对外导出内容为module.exports,如果对module.exports重写,则会覆盖exports上的内容。
循环模块
当出现模块互相依赖出现循环时,需要做特殊处理打破这种循环,但又要保证两个模块能成功加载,官方示例:
a.js:
console.log(‘a starting');
exports.done = false;
// a 循环依赖b, 此时会退出a的加载,回到外层
const b = require(‘./b.js’);
console.log(‘in a, b.done = %j’, b.done);
exports.done = true;
console.log(‘a done’);
b.js:
console.log(‘b starting');
exports.done = false;
const a = require(‘./a.js’);
// 加载a, 只有部分执行代码的导出内容
console.log(‘in b, a.done = %j’, a.done);
exports.done = true;
console.log(‘b done’);
// b结束后会继续执行a中require('b')的后续逻辑
main.js:
console.log(‘main starting');
const a = require(‘./a.js’);
const b = require(‘./b.js’);
console.log(‘in main, a.done = %j, b.done = %j’, a.done, b.done);
当执行main.js时,其执行结果为:
$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true
说明: 当出现循环时,会首先返回a的一个未完成状态的exports对象给b,让b完成加载,当b完成后则可以将其完整的exports给a,让a完成加载。
参考
Requiring modules in Node.js: Everything you need to know
How the module system, CommonJS & require works | @RisingStack
Modules | Node.js v14.0.0 Documentation
node/loader.js at master · nodejs/node · GitHub
