前面几篇文章分别介绍了CommonJS和ES6 Module两种形式的模块定义,这篇将介绍下两者各自的特性。
动态与静态
CommonJS与ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”。在这里“动态”的含义是,模块依赖关系是建立发生在代码运行阶段;而“静态”则表示模块依赖关系的建立发生在代码编译阶段。
// calculator.js
module.exprots = {name: 'calculator'};
// index.js
const name = require('./calculator.js').name;
console.log(name);
模块index在加载模块calculator时会执行模块calculator的代码,并将其module.exports对象作为require函数的返回值返回。require的模块路径可以动态指定,支持传入一个表达式,甚至可以通过if语句判断是否加载某个模块。因此,在CommonJS模块被执行前,不能确定之间的依赖关系,模块的导入、导出发生在代码的运行阶段。
// calculator.js
export const name = 'calculator';
// index.js
import { name } from './calculator.js';
ES6 Module的导入、导出语句都是声明式的,它不支持将表达式作为导入路径,并且导入、导出语句必须位于模块的顶层作用域,也不能放在if语句中。因此说,ES6 Module是一种静态的模块结构,在ES6代码的编译阶段就可以分析出模块的依赖关系。
相比CommonJS,ES6 Module具备以下优势:
- 死代码检测和排除。开发者可以使用静态分析工具检测出哪些模块没有被调用过。比如,在引入工具类库时,工程中往往只用到了其中的一部分组件或接口,但有可能会将其代码完整地加载进来。未被调用到的模块代码永远不会被执行,也就会成龙死代码。通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积。
- 模块变量类型检查。JavaScript属于动态类型语言,不会在代码执行前检查类型错误。ES6 Module的静态模块结构有助于确保模块之间传递的值或接口类型时正确的。
- 编译器优化。在CommonJS等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而ES6 Module支持直接导入变量,减少了引用层级,程序效率更高。
值复制与动态映射
在导入一个模块时,对于CommonJS来说获取的是一份导出值得副本;而在ES6 Module中则是值的动态映射。并且这个映射是只读的。
可以通过示例来了解一下什么是CommonJS中的值复制。
// calculator.js
var count = 0;
module.exoports = {
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(5,6);
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
let count = 0;
const 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(5,6);
console.log(count); // 输出 1 (实时反映calculator.js中count值得变化)
count += 1; // 不可更改,会抛出SyntaxError:"count" is readonly
上面的额示例展示了ES6 Module中导入的变量其实是对原有值得动态映射。index.js中count是对calculator.js中count值得实时反映,当通过调用add函数更改了calculatot.js中的count值时,index.js中count的值也随之变化。并且ES6 Module规定不能对导入的变量进行修改。
循环依赖
循环依赖是指模块A依赖于模块B,同时模块B依赖于模块A。
一般来说工程中应该避免循环依赖的产生,因为从软件设计的角度来说,单向的依赖关系更加清晰,循环依赖则会带来一定的复杂度。但是在实际开发中,循环依赖有时会在不经意间产生,因为当工程的复杂度上升到足够大时,就容易出现隐藏的循环依赖关系。
首先通过示例来了解一下循环依赖在CommonJS中的表现。
// foo.js
const bar = require('./bar.js');
console.log('value of bar:',bar);
module.exports = "This is foo.js";
// bar.js
const foo = require('./foo.js');
console.log("value of foo:", foo);
module.exoports = "This is bar.js";
// index.js
require('./foo.js');
在示例中,index.js是执行入口,它加载了foo.js,foo.js和bar.js之间存在循环依赖。
开发者的本意是想在控制台输出
value of foo: This is foo.js
value of bar: This is bar.js
而实际运行上面的示例,输出的却是
value of foo: {}
value of bar: This is bar.js
结果为什么会与预期的不一样呢?梳理下代码的实际的执行顺序就会明白了。
- index.js导入了foo.js,此时开始执行foo.js中的代码。
- foo.js的第一句导入了bar.js,这时foo.js不会继续向下执行,而是会进入bar.js内部。代码的执行权就到了bar.js。
- 在bar.js中又对foo.js进行了导入,这里产生了循环依赖。需要注意的是,代码的执行权并不会再交回给foo.js,而是直接取其导出值,也就是module.exports。但是由于foo.js中的代码未执行完毕,导出值在这时为默认的空对象,因此当bar.js执行到打印语句时,控制台就会输出一个空对象,即
value of foo: {}
- 当bar.js中的代码执行完毕,代码的执行权就会交回给foo.js,foo.js中的代码从require语句的下一句开始执行,也就是foo.js中的输出语句开始执行,由于bar.js中的代码已经执行完毕,bar.js中的module.exports已经被赋值,此时控制台输出
value of bar: This is bar.js
是正确的。foo.js中的代码执行完毕,整个流程结束。
通过代码的执行顺序可以看出,尽管循环依赖的模块被执行了,但是模块导入的值并不是开发者想要的。因此如果再CommonJS中遇到循环依赖,将没有办法得到预想中的结果。
提示
为什么在步骤3中,直到bar.js中代码执行完毕才会将代码的执行权交回给foo.js呢?这就跟webpack模块加载机制有关了。Webpack加载模块时,会首先判断模块是否已被加载,如果该模块事先没有被加载过,就加载该模块,执行该模块中的代码;如果该模块事先已被加载,则直接返回该模块的导出值,即module.exports;
接下来通过ES6 Module的方式改写上面的示例。
// foo.js
import bar from "./bar.js";
console.log("value of bar:",bar);
export default "This is foo.js";
// bar.js
import foo from "./foo.js";
console.log("value of foo:",foo);
export default "This is bar.js";
// index.js
import foo from ""./foo.js;
理想状态下,示例的实际运行结果:
value of foo: undefined
value of bar: This is bar.js
代码的执行顺序与之前的并无太大的区别。区别在于在ES6 Module中,只是生成一个指向被加载模块的引用,代码未执行时,这个引用的值就是undefined。所以当代码执行到bar.js中的console.log("value of boo:",boo)
时,boo的值就是undefined。
提示
示例代码如果运行在Webpack环境下,会出现异常报错:Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization
。这时因为在webpack编译时,会把 es6 降级为 es5,降级处理方式上可能会有一定的差异。并且,在ES6 Module中处理引用的方式与Webpack也不相同。在ES6 Module的处理方式是,先静态分析import,然后动态export导出(导出引用),webapck的处理方式是,先将所有export提到了模块的开始,然后import提升。
示例代码仓库
https://gitee.com/zero_79152105/webpack-vblog
原创文章,作者:ZERO,如若转载,请注明出处:https://www.edu24.cn/course/webpack-commonjs-esm.html