从两个问题来理解commonJS规范,收获不小

137 阅读3分钟

前言

这篇文章讲了commonJS中两个不太容易理解的问题,先是从概念的角度去谈,然后看用es5实现的commonJS。相信看完后,会收获不小的

exports和module.exports的区别

起初export和module.exports是指向同一个对象,作用是导出模块内部的变量。

用法:

//a.js
exports.name = 'zenos';
module.exports.age = '18';
//index.js
const {name, age} = require('./a.js');

console.log(name,age);
//zenos 18

在exports或者module.exports上面挂载要导出的属性,效果是一致的。但如果是下面这种用法就有点不一样了。

//a.js
exports = {
  name: 'zenos'
}
//b.js
module.exports = {
  age : '18'
}
//index.js
const Obja = require('./a.js');
const Objb = require('./b.js');

console.log(Obja);
// {}
console.log(Objb);
//{ age : '18'}

可以看到,如果用对象赋值的形式,只有module.exports会生效,而exports不会生效。这是为什么呢?

这是因为我们用require导入的时候,就是导入module.exports,如果像a.js的用法,就会导致exports变量指向新的对象,而不是原来的module.exports。所以就会出现上面看到的情况。

循环依赖

" 在commonJS中,循环依赖会导致死循环吗? ",这是我之前经常问自己的问题。

现在搞明白了,不会死循环。

//a.js
const Objb = require('./b.js');
console.log(Objb);
//b.js
require('./a.js');
exports.name = 'b';
node b.js

首先我们要明白,每次require一个模块,node都会先去一个对象(可以把它叫做module_cache)里面找,如果找到了,就直接返回。如果没有找到,就去装载。

装载的过程:先在cache对象里放module.exports对象,现在这个对象是空的。然后找到该文件,编译该文件,执行该文件。之后,如果文件里面有在module.exports对象上面挂载东西,那其他模块就能通过require拿到。

注意这个顺序很重要,是首先在生成一个空的module.exports对象,然后再去执行文件的代码

文件b.js执行的过程:

  1. 编译执行b.js
  2. 遇到了require(' ./a.js')
  3. 在cache对象里面找属于a.js的module.exports对象。没有找到,装载a.js
  4. 在装载a.js过程,遇到了require('./b.js')
  5. 在cache对象里面找属于a.js的module.exports对象,找到了。

虽然找到了,但是一个空对象,因为b.js现在还没有在上面挂载东西

  1. 执行console.log(Objb)

可以预见打印结果为{}

  1. 装载a.js完毕,返回属于它的module.exports对象(也是空的)
  2. 执行exports.name = 'b',在上面挂了一个name
  3. b.js执行完毕

简单吧,哈哈😄

__filename和__dirname的区别

__filename:返回该文件的绝对路径;

__dirname:返回该文件所处目录的绝对路径;

现在有个a.js文件,其目录为/user/project/a.js

//a.js
console.log(__filename); // /user/project/a.js
console.log(__dirname);	// /user/project

如何查看用es5对commonJS的实现

理解一个规则的最好办法就是看下它是如何实现的。这里我们需要借用webpack,webpack可以将commonJS的语法转成用es5表示

初始化npm项目,安装webpack依赖

npm init -y
npm install webpack webpack-cli

打包上面的a.jsb.js

npx webpack b.js

找到dist/bundle.js就可以看见转义之后的代码了

分析代码

下面是上面webpack编译出来的代码。实际的代码的变量名称不太好看明白,所以更改一变量的名称让代码更好懂

(() => {
  var webpack_module = {
    './a.js': (module, exports, require) => {
      const t = require('./b.js');
      console.log(t);
    },
    './b.js': (module, exports, require) => {
      require('./a.js'), (exports.name = 'b');
    },
  };
  
  var cache = {};
  
  function require(module_name) {
    var n = cache[module_name];
    if (void 0 !== n) return n.exports;
    var s = (cache[module_name] = { exports: {} });
    webpack_module[module_name](s, s.exports, require);
    return s.exports;
  }
  
  require('./b.js');
  
})();

解释:

  1. webpack_module对象是储存模块代码的对象,将模块的路径当作key,value是函数,函数内部就是我们在文件里面编写的代码。函数的参数有module、export、require,这就是我们在文件中调用的导入导出时用到的变量
  2. cache是用来储存模块导出的exports的对象
  3. require函数的功能就是装载,装载的过程我上面讲过了,就不重复了。

通过编译后的代码来回看上面的问题

  1. exports和module.exports的区别
  2. 循环依赖

是不是有了更深刻的理解了,哈哈 🕶

总结

通过webpack编译后的es5源码,来理解commonJS不太好懂的规则

  1. exports和module.exports的区别
  2. 循环依赖