【Webpack】模块打包 CommonJS和ES6 Module的区别

发布时间:2023年12月29日

CommonJS与ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”而后者是“静态的”。
动态”的含义是,模块依赖关系的建立发生在代码运行阶段;
而“静态”则是模块依赖关系的建立发生在代码编译阶段

先看一个CommonJS的例子

// calculator.js
module.exports = {name:'calculator'};
// index.js
const name = require(./calculator.js).name;

在前面提到过,require的模块路径可以动态指定,支持传人一个表达式,甚至可以通过if语句判断是否加载某个模块。因此,在CommonJS模块被执行前,并没有办法确定明确的依赖关系,模块的导人、导出发生在代码的运行阶段。

ES6 Module的导人、导出语句都是声明式的,它不支持导人的路径是一个表达式并且导人、导出语句必须位于模块的顶层作用域,因此我们说,ES 6Module是一种静态的模块结构,在ES6代码的编译阶段就可以分析出模块的依赖关系

相比于CommonJS来说具备以下几点优势:

  1. 死代码检测和排除。我们可以用静态分析工具检测出哪些模块没有被调用过比如,在引人工具类库时,工程中往往只用到了其中一部分组件或接口,但有可能会将其代码完整地加载进来。未被调用到的模块代码永远不会被执行,也就成为了死代码。通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积
  2. 模块变量类型检查。JavaScript属于动态类型语言,不会在代码执行前检查类型错误(比如对一个字符串类型的值进行函数调用)。ES6 Module的静态模块结构有助于确保模块之间传递的值或接口类型是正确的
  3. 编译器优化。在CommonJS等动态模块系统中,无论采用哪种方式,本质上导人的都是一个对象,而ES6 Module支持直接导人变量,减少了引用层级,程序效率更高

值拷贝与动态映射
在导人一个模块时,对于CommonJS来说获取的是一份导出值的拷贝,而在ES6 Module中则是值的动态映射,并且这个映射是只读的。
要理解上面的概念,看下面的例子

CommonJS中的值拷贝。

// calculator.js
var count = 0
module.exports = {
	count: count,
	add: function(a,b){
		count+=1
		return a+b
	}
}

// index.js
var count = require('./calculator.js').count
var add = require('./calculator.js').add

console.log(count); // 0 (这里的count是对 calculator,js 中count 值的持贝)
add(23);
console,log(count); // 0 (calculator.js中变量值的改变不会对这里的拷贝值造成影响)
count +=1;
console.log(count);// 1(拷贝的值可以更改)

index.js中的count是对 calculator.js 中count的一份值拷贝,因此在调用add函时,虽然更改了原本calculator.js中count的值但是并不会对index.js 中导人时创建的副本造成影响
另一方面,在CommonJS中允许对导人的值进行更改。我们可以在index.js更改count和add,将其赋予新值。同样,由于是值的拷贝,这些操作不会影响 calculator.js本身

ES6 Module

// calculator.js
var count = 0
module.exports = {
	count: count,
	add: function(a,b){
		count+=1
		return a+b
	}
}
export{count, add}

// index.js
import ( count,add ) from './calculator.js';
console.log(count);//0(对 calculator.js中 count 值的映射)
add(23);
console.log(count);//1 (实时反映calculator.js 中count值的变化)
// count +=1; //不可更改,会抛出SyntaxError:"count"is read-only

index.js中的count是对calculator.js中的count值的实时反映,当我们通过调用add 函数更改了calculator.js中count值时,index,js 中count的值也随之变化

我们不可以对ES6 Module导人的变量进行更改,可以将这种映射关系理解为一面镜子,从镜子里我们可以实时观察到原有的事物,但是并不可以操纵镜子中的影像

循环依赖

循环依赖是指模块A依赖于模块B,同时模块B依赖于模块A。比如下面这个例子:

//a.js
import ( foo )from './b,js';
foo();

// b.js
import ( bar ) from./a.js';
bar();

一般来说工程中应该尽量避免循环依赖的产生,因为从软件设计的角度来说,单向的依赖关系更加清晰,而循环依赖则会带来一定的复杂度。而在实际开发中,循环依赖有时会在不经意间产生,因为当工程的复杂度上升到足够规模时,就容易出现隐藏的循环依赖关系

简单来说,A和B两个模块之间是否存在直接的循环依赖关系是很容易被发现的。但是当中间模块太多时就很难发现A和B之间存在着隐式的循环依赖。

那如何解决循环依赖的问题
其实可以利用ES6 Module的特性是其支持循环依赖,看下面的例子

// index.js
import foo from './foo.js'
foo('index.js')

// foo.js
import bar from './bar.js'
function foo(invoker){
	console.log(invoker + ' invokes foo.js';
	bar('foo.js');
}
export default foo;

// bar.js
import foo from './foo.js'
let invoker = false
function bar(invoker){
	if(!invoker){
		invoker = true;
		console.log(invoker + ' invokes bar.js';
		foo('bar.js');
	}
}
export default bar;


上面代码的执行结果如下:

indexjs invokes foo.js
foo.js invokes bar.js
bar.js invokes foo.js

可以看到,foo.js和bar.js 这一对循环依赖的模块均获取到了正确的导出值。下面让我们分析一下代码的执行过程。

  1. indexjs作为人口导人了 foojs,此时开始执行 foo.js 中的代码。
  2. 从foo.js导人了bar.js,执行权交给bar.js。
  3. 在bar.js 中一直执行到其结束,完成bar 函数的定义。注意,此时由于 foo.js还没执行完,foo的值现在仍然是undefined。
  4. 执行权回到foo.js继续执行直到其结束,完成foo函数的定义。由于ES6动态映射Module动态映射的特性,此时在bar.js中foo的值已经从undefined成为了我们定义的函数,这是与CommonJS在解决循环依赖时的本质区别,CommonJS中导人的是值的持贝,不会随着被夹在模块中原有值的变化而变化
  5. 执行权回到index.js并调用foo函数,此时会依次执行 foo->bar一>foo,并在控制台打出正确的值。

由上面的例子可以看出,ES6 Module的特性使其可以更好地支持循环依赖,只是需要由开发者来保证当导人的值被使用时已经设置好正确的导出值

文章来源:https://blog.csdn.net/loyd3/article/details/135249324
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。