Node CommonJS模块源码解析

在 CommonJS 规范中,一个文件就可以作为一个独立的模块,有自己的作用域,在这个文件内部定义的变量、函数等,都只属于这个模块,对其他模块是不可见的。如果想要其他模块能使用其内部的变量,就需要使用 module.exports 导出,然后在其他模块中使用 require()导入。

因为 Node 就是 CommonJS 规范的一种具体实现,以下我们主要使用 Node 的 CommonJS 模块来讲解 CommonJS 规范下模块化的思维。


一、常见问题解析

这里我们先抛出一些问题,然后跟着问题,再一步步去解析 Node 的 CommonJS 模块机制:

1. Node中的CommonJS模块是啥,一个模块中包含哪些信息?

2. exports和module.exports有什么联系和区别?

3. 怎么判断当前模块是根模块还是子模块?

4. 多次引入同一个模块时,这个模块内部的代码是否会多次执行?

5. 两个模块循环引用时,是否会造成死循环?

6. 模块的引入是同步的还是异步的?

7. require默认支持导入哪些文件类型?

8. 模块中的exports、require、module、__filename、__dirname从哪儿来,为什么可以不定义就直接用?

9. 为什么说模块内部的变量对其他模块不可见?

10. 调用require(id)时,传入的id的解析规则?

1. CommonJS 模块(module)

Node 中,有一个构造函数:Module,我们所用的每一个模块,其实都是由这个构造函数 new 出来的一个module 实例,用变量module表示。Module 构造函数的源码如下:

function Module(id = "", parent) {
   
  // id通常传入的都是文件的绝对路径
  this.id = id;
  this.path = path.dirname(id);
  this.exports = {
   };
  // 这个 updateChildren 的作用,就是把创建出来的模块实例,添加到父模块的children列表中
  updateChildren(parent, this, false);
  this.filename = null;
  this.loaded = false;
  this.children = [];
}

另外 Module 构造函数的原型上还有 3 个方法:

// 1. require:用于引入模块的
Module.prototype.require = function(id) {
   };

// 2. load:用于加载模块的
Module.prototype.load = function(filename) {
   };

// 3. _compile:用于编译、执行模块代码的
Module.prototype._compile = function(content, filename) {
   };

稍后我们会详解这 3 个方法。我们可以看到,当调用new Module()时,实例化出来的 module 模块,其结构大致如下:

module = {
   
  id: ".", // 模块id,通常为文件的绝对路径,根模块会被重置为'.'
  path: "", // 模块所在文件夹的路径
  exports: {
   }, // 导出的模块值,初始值为{}
  parent: {
   }, // 父模块(Node文档中说已弃用,不过实际的值还是可以获取到)
  filename: "", // 模块文件的绝对路径(这个属性值是等到加载模块的时候添加上来的)
  loaded: false, // 标识当前模块是否已经加载完毕
  children: [], // 子模块列表
  // paths的值,是从当前文件夹下,依次往上遍历,直到根目录,每级目录下的node_modules目录。
  //(当require(path)传入的是第三方模块的时候会用到,这个属性值也是在等到加载模块的时候添加上来的)
  paths: [
    // 'D:\\WEB_NOTES\\modules\\Commonjs\\node_modules',
    // 'D:\\WEB_NOTES\\modules\\node_modules',
    // 'D:\\WEB_NOTES\\node_modules',
    // 'D:\\node_modules'
  ],
  __proto__: {
   
    require: function(id) {
   },
    load: function(filename) {
   },
    _compile: function(content, filename) {
   },
  },
};

我们可以看到,在这个实例上,有一个exports属性,初始值为空对象,module.exports的值,就是我们最终导出的模块,然后在其他模块中使用require()导入时,module.exports就会被作为 require 函数的返回值返回出去。如下:

// moduleA.js
// 在此文件中导出模块
module.exports = {
   
  name: "I am moduleA",
};
// index.js
// 在此文件中引入模块moduleA
const moduleA = require("./moduleA.js");

// 输出模块moduleA的值,我们可以看到moduleA的值就是我们在moduleA.js中导出的对象
console.log("moduleA: ", moduleA); // moduleA:  { name: 'I am moduleA' }

2. module.exports 和 exports

用过 Node 的人,对这 2 个 api 应该都不会陌生,在实际使用场景中,这 2 个 api 的使用方式也是很容易混的,稍不注意就可能用错了,那么这 2 个 api 究竟有什么联系和区别呢?

其实在 Node 的源码中,有几行代码能很好的解释他们的关系:

// 1. 用 = 赋值,使 exports 成为 module.exports 的一份副本
// 其中`this`就是我们的 module 实例
const exports = this.exports; // 等价于 const exports = module.exports;

// 2. require函数的返回值如下(这里有做逻辑上的简化处理)
const require = function(id) {
   
  // ...
  return module.exports;
};

从上面我们就可以很清晰的看到: exportsmodule.exports都指向同一个地址,他们的初始值为一个空对象,但是要注意最终返回的是module.exports,这也就意味着实际使用场景中会有如下几个需要注意的地方:

  1. 因为 exports 与 module.exports 都是对象,所以我们使用 exports.key = valuemodule.exports.key = value 的形式给导出模块赋值,效果都是等价的,
// moduleA.js
module.exports.name = "I am moduleA";
exports.age = 20;
// index.js
const moduleA = require("./moduleA.js");
console.log("moduleA: ", moduleA); // moduleA:  { name: 'I am moduleA', age: 20 }

我们可以看到,name 和 age 的值都正常导出了。

  1. 因为require函数最终返回的是module.exports的值,所以如果我们使用了module.exports = newValue的形式给其重新赋了值,就会导致module.exportsexports的联系断掉。这时,如果我们还使用了exports.key = value的形式给导出模块赋值,就会导致exports.key的值丢失:
// moduleA.js
exports.age = 20;

module.exports = {
   
  score: 100,
};
// index.js
const moduleA = require("./moduleA.js");
console.log("moduleA: ", moduleA); // moduleA:  { score: 100 }

所以,如果我们要使用module.exports = newValue的形式导出模块,就不要再使用exports

  1. 不能使用exports = value的形式导出模块,不然也会导致module.exportsexports的联系断掉,致使exports = value的 value 值丢失:
// moduleA.js
exports = 20;

module.exports.score = 100;
// index.js
const moduleA = require("./moduleA.js");
console.log("moduleA: ", moduleA); // moduleA:  { score: 100 }

3. 怎么判断当前模块是根模块还是子模块?

在 require 函数上,有一个main属性,它指向了当前模块引用链上的根模块,通过require.main === module来判断,如果当前模块是根模块则返回 true,子模块返回 false。

4. 多次引用同一个模块(模块缓存)

在模块的引用机制内部,当一个模块成功加载一次之后,就会被写入缓存,具体缓存信息,可以打印require.cache查看;第二次加载的时候,就会直接从缓存读取数据,而不会再次加载、执行模块内部的代码。所以,多次引入同一个模块时,这个模块内部的代码不会多次执行。如下:

//  moduleA.js
module.exports.time = Date.now();
//  index.js
const moduleA1 = require("./moduleA");

console.log("moduleA1 = ", moduleA1);

console.log("require.cache = ", require.cache);

const moduleA2 = require("./moduleA");

console.log("moduleA2 = ", moduleA2);

// 上面的打印结果输出如下:
// console.log("moduleA1 = ", moduleA1);
moduleA1 = {
    time: 1606017634808 }

// console.log("require.cache = ", require.cache);
require.cache: {
   
  'E:\\WEB_NOTES\\modules\\Commonjs\\index.js': {
   
    id: '.',
    path: 'E:\\WEB_NOTES\\modules\\Commonjs',
    exports: {
   },
    parent: null,
    filename: 'E:\\WEB_NOTES\\modules\\Commonjs\\index.js',
    loaded: false,
    children: [ [Module] ],
    paths: [
      'E:\\WEB_NOTES\\modules\\Commonjs\\node_modules',
      'E:\\WEB_NOTES\\modules\\node_modules',
      'E:\\WEB_NOTES\\node_modules',
      'E:\\node_modules'
    ]
  },
  'E:\\WEB_NOTES\\modules\\Commonjs\\moduleA.js': {
   
    id: 'E:\\WEB_NOTES\\modules\\Commonjs\\moduleA.js',
    path: 'E:\\WEB_NOTES\\modules\\Commonjs',
    exports: {
    time: 1606017634808 },
    parent: Module {
   
      id: '.',
      path: 'E:\\WEB_NOTES\\modules\\Commonjs',
      exports:
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值