模块基本数据结构

carbon (6).png
其中children中的Module表示正常模块,当出现Cycular时表示是循环依赖的模块。

源码中Module定义module:

  1. function Module(id = '', parent) {
  2. this.id = id;
  3. this.path = paht.dirname(id);
  4. this.exports = {};
  5. this.parent = parent;
  6. updateChildren(parent, this, false);
  7. this.filename = null;
  8. this.loaded = false;
  9. this.children = []
  10. }

module的相关属性会在load和compile时赋值。

核心模块
核心模块即内置在Node源码中的相关模块,统一存放在lib/目录下,而且核心模块只需要通过其标识符即可require(),如require(‘http’)会返回内置的HTTP模块。

模块解析过程

Module._load:
检查是否缓存过,有缓存则直接返回缓存内容;否则判断是否原生模块,是则调用NativeModule.require()方法并返回结果;
否则创建一个新的模块new Module()并保存到缓存中,加载文件内容并解析

Module._compile:

该函数负责解析模块的文件内容,其实是在指定的上下文环境中执行文件内容,并对其进行包装
node-js-at-scale-how-require-works.png

  • Resolution: 确定文件路径
  • Loading: 确定文件类型
  • Wrapping: 定义局部作用域
  • Evaluation: VM执行文件内容
  • Caching: 缓存文件内容

Loading时会根据文件类型不同确定对应的加载函数:

  1. // Native extension for .js
  2. // .js文件加载过程
  3. Module._extensions[‘.js’] = function(module, filename) {
  4. if (filename.endsWith('.js')) {
  5. const pkg = readPackageScope(filename);
  6. // Function require shouldn’t be used in ES modules.
  7. if (pkg && pkg.data && pkg.data.type === module’) {
  8. const parentPath = module.parent && module.parent.filename;
  9. const packageJsonPath = path.resolve(pkg.path, package.json’);
  10. throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
  11. }
  12. }
  13. // 读取文件内容并执行”编译”
  14. const content = fs.readFileSync(filename, utf8’);
  15. module._compile(content, filename);
  16. };
  17. // Native extension for .json
  18. Module._extensions[‘.json’] = function(module, filename) {
  19. const content = fs.readFileSync(filename, utf8’);
  20. if (manifest) {
  21. const moduleURL = pathToFileURL(filename);
  22. manifest.assertIntegrity(moduleURL, content);
  23. }
  24. try {
  25. module.exports = JSONParse(stripBOM(content));
  26. } catch (err) {
  27. err.message = filename + ‘: + err.message;
  28. throw err;
  29. }
  30. };
  31. // Native extension for .node
  32. Module._extensions[‘.node’] = function(module, filename) {
  33. if (manifest) {
  34. const content = fs.readFileSync(filename);
  35. const moduleURL = pathToFileURL(filename);
  36. manifest.assertIntegrity(moduleURL, content);
  37. }
  38. // Be aware this doesn’t use `content`
  39. return process.dlopen(module, path.toNamespacedPath(filename));
  40. };

在模块真正执行前,都会被包装一个外部函数,如下:

(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]
}

这样做首先可以保证变量定义是模块内局部的,不会跑到全局对象;其次,moduleexports可以用来对外提供导出值,而__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