模块基本数据结构
其中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 .json
Module._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 .node
Module._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