Commonjs与Es6Module的区别 以及延伸出的循环依赖

121 阅读4分钟

Commonjs与Es6Module区别1: (最本质的)

前者在模块的处理上是动态的后者则是静态的

  • 动态的含义: 模块间的依赖关系建立发生在代码运行阶段
  • 静态的含义: 模块间的依赖关系建立发生在代码编译阶段

来几个列子论证一下

- Commonjs例子

 //A.js
const name = require('./B.js').name
//B.js
module.exports = {name: 'commonjs'}

当模块A加载模块B时,会执行模块B的代码 require函数返回值 = module.exports 返回对象, 甚至我们可以通过条件判断返回的模块,因此在commonjs模块没执行前 我们是无法明确依赖关系的

- Es6Moudle例子

//A.js
import {name} from './B.js'
//B.js
export const name = 'Es6Modulejs'

es6的模块导入是声明式 不支持表达式。并且导入、导出的语句必须在模块顶层作用域。

-Es6Module优势:

死代码检测和排除。可以通过静态分析工具检测模块有没有被调用,减少包的体积
模块变量类型检查。javascript是动态类型语言,无法在代码执行前检测类型错误(对字符串变量进行函数调用)。Es6Module是静态模块结构有助于确保模块间值和接口类型的正确性
编译器优化。 Commonjs动态模块 本质上导出的是一个对象 而Es6module直接导出变量,减少了层级引用。

Commonjs与Es6Module区别2: 值拷贝与动态映射

Commonjs: 获取的是值拷贝 Es6Module:获取的是值的动态映射,并且这个值是只读的。

来几个列子论证一下

- Commonjs

//B.js
var count = 0
moudule.exports = {
count: count
add: function (a, b) {
  count+=1
  return a + b
}
}
//A.js
var count = require('./B.js').count
var add = require('./B.js').add
add(2,3)
console.log(count) //0(B.js中被拷贝的值没有被修改)
count+=1
console.log(count) //1(拷贝的值可以修改)

A.js中的count是值拷贝,虽然add改变了B.js中的count,但是并不会对A.js中的造成影响

- Es6Module

//B.js
let count = 0
const add = (a,b) => {
  count+=1
  return a+b
}
export {count,add}
//A.js
import {count,add} from './B.js'
add(2,3)
console.log(count) //1 (B.js中count的映射)
count+=1 //抛异常 ‘count’ is read-only

A.js中的count是对B.js中count的动态映射,可以理解为B.js值的实时反应,但是不可以做出修改,打个比方:A是一面镜子,可以观察镜子中值的变化,但是不可以操控镜子中的镜像

Commonjs与Es6Module区别3: 延伸出的循环依赖

如何处理循环依赖是开发者必须面对的问题!我会对Commonjs和Es6Module中的循环依赖做对比

- Commontjs:

//A.js
require('./B.js')
​
//B.js
const bar = require('./C.js')
console.log('B.js',bar)
module.exports = 'this is moduleB'//C.js
const foo = require('./B.js')
console.log('C.js',foo)
module.exports = 'this is moduleC'

输出:C.js {}; B.js this is moduleC

why?

  1. A.js 引用了 B.js 进入B.js 执行代码
  2. B.js 第一行引用了 C.js 进入C.js 不再继续执行B.js剩下的代码
  3. C.js 第一行引用了 B.js(产生循环依赖) 不会再次进入B.js了 直接用B.js默认的导出( {} ) 继续执行C.js剩下的代码直到结束
  4. C.js 执行结束 将控制权交回B.js 执行结束

- 提问:

为什么第三步不会再次进入B.js 使程序陷入死循环呢?

Good 我们把这段代码放入webpack中打包后,bundle中有酱紫一段代码:

var __webpack_module_cache__ = {};

  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // Create a new module (and put it into the cache)
    var module = __webpack_module_cache__[moduleId] = {
      // no module.id needed
      // no module.loaded needed
      exports: {}
    };

    // Execute the module function
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
​
    // Return the exports of the module
    return module.exports;
  }

从bundle中可以看出 代码会判断 cachedModule 是否加载过 加载过就不会再次加载了。

- 结论:

酱紫我们就可以打断点 瞅瞅__webpack_module_cache__在执行结束未销毁的时候长什么样子了。可以看出B.js和C.js暴露出来的东东.

image-20221009175121763.png

- Es6Moudle:

    ```
    //A.js
    import foo from './B.js'//B.js
    import bar from './C.js'
    console.log('B.js',bar)
    export default 'this is moduleB'//C.js
    import foo from './B.js'
    console.log('C.js',foo)
    export default 'this is moduleC'
    ```

输出C.js undefined; B.js:8 B.js this is moduleC

很遗憾C.js也没有输出,只不过和Commonjs不同的是这里获取到的是undefined,在bundle中可以看到exports["default"] = void 0;默认导出的是undefined.

-提问:

期望用循环依赖输出C.js this is moduleB; B.js:8 B.js this is moduleC有没有什么办法呢?

//A.js
import foo from './B.js
foo('A.js this is moduleA')
​
//B.js
import bar from './C.js'
const foo = (invoker) => {
  console.log(invoker)
  bar('B.js this is moduleB')
}
export default foo
​
//C.js
import foo from './B.js'
let invoked = false
const bar = (invoker) => {
  if(!invoked){
    invoked = true
    console.log(invoker )
    foo('C.js this is moduleC')
  }
}
export default bar

输出:A.js this is moduleA; B.js this is moduleB; C.js this is moduleC

  1. A.js作为入口,加载A.js, 第一行引入B.js, 将控制权交给B.js
  2. 加载B.js代码,第一行引入C.js, 讲控制权交给C.js
  3. C.js一直执行到完毕,完成了对bar函数的定义,此时C.js的foo函数还是undefined, 将控制权交还给B.js
  4. B.js 继续完成对foo函数的定义,将控制权交还A.js 执行foo('A.js this is moduleA') 此时foo 和 bar函数都已经初始化完成。
  5. 依次执行 foo('A.js this is moduleA') => bar('B.js this is moduleB') => foo('C.js this is moduleC') => bar('B.js this is moduleB') 但是此时invoked=true

-结论:

上述例子可以看到ES6的特性更好的支持了循环依赖,只需要开发者保证当值被使用时,已经导出了正确的值!
当然在现实开发过程中,我们是严禁产生循环依赖的,使用webpack中插件circular-dependency-plugin来检测是否有循环依赖。