ESM和CJS区别
ES6 Module和CommonJS区别
区别一
前者属于编译时加载,即静态加载,在编译时就能够确定模块的依赖关系,以及输入和输出的变量;后者属于运行时加载,都只有在代码运行时才能确定这些东西。ESM形式的好处是可以做到tree shaking。
区别二
前者可以加载模块的部分内容,后者需要加载模块整个对象,再取到内容。
区别一和二可以用tree shaking来解释一下是大概是怎样一个流程。
按照webpack官网的解释,tree shaking通常用于描述移除JavaScript上下文中的未引用代码(dead-code)。它依赖于ESM的静态分析能力,例如import和export。用大白话解释就是,如果是使用模块化开发的话,就可以删除那些引入某个模块中用不到的函数。
为什么叫tree shaking, 我个人觉得这个名次叫的很形象呀。你可以将应用程序想象成一棵树。绿色表示实际用到的源码和 library,是树上活的树叶。灰色表示无用的代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。
1.index.js的作用是 创建一个DOM元素,然后引入math里面的cube函数来使用,只用了cube函数。
import { cube } from './math'
function component() {
var element = document.createElement('div')
element.innerHTML = [
'hello webpack!',
'5 cubed is equal to' + {cube(5)
].join('\n\n')
return element
}
document.body.appendChild(component())
而在math.js里,export了两个工具函数。实际上我们要看的就是,只有cube在index里面使用了,但是看下square在不同的模块系统下是否被引入。
export function square (x) {
return x * x
}
export function cube (x) {
return x * x * x
}
最后我们运行npm run build后,会在dist目录下生成app.bunble.js到文件,这个文件是被压缩过的,我这边格式化下代码。
根据结果我们看到,index.js里面只使用了cube函数,在最终的结果bundle.js中,也只把math中的cube函数引入了进来,而没有处理square函数。因为它没有在实际过程中使用到。这就通过ESM的import和export的方式实现了tree shaking。也就是我们在区别一、二中说到的,ESM可以在编译时静态分析模块之间的依赖,并且只加载模块中被使用的内容。
2.ok看到这里,我们已经实现了tree shaking。但是我们要琢磨下,不同的import和export方式或者使用CommonJS的情况下,能否实现tree shaking。
我们把index.js里面的引入方式改成如下
import * as math from './math'
// 使用的方式改成math.cube(5)
math.js的export方式不变,但是我们改变了index.js的引入方式。在运行下npm run build后,我们发现bundle.js也只是输出了cube函数,square函数就被tree shaking掉了。实际上跟上面那种的结果是一样的。
改变index.js中引入math的方式
3. 按照ESM的语法规定,我们可以混用export和export default,那么我们在math.js再加上export default ,index.js引入math的方式可以使用两种。
// math.js
export ..
export ..
export default {
square,
cube
}
// index.js
import { cube } from './math'
// 或者 import math from './math'
运行npm run build后,格式化bunble.js文件,我们会发现当我们只引入cube函数时,可以做到tree shaking,但是当我们直接import math来后,就把没有用到的square函数也引入了。
import { cube } from './math'的方式
import math from './math'的方式
仔细想想,这也就说明了ESM要实现加载模块的部分内容,export某个模块是ok的,但是export default整个模块出来,就无法做tree shaking了。
4. 最后看看CommonJS的结果是怎样的。我们知道,CommonJS属于运行时加载,它会在代码运行到那一行时将整个模块加载进来。我们将index.js的引入方式改成如下,而math还是按照原始状态,单独export 两个 工具函数。
// index.js
const { cube } = require('./math')
// 写法二:const math = require('./math')
// math.js
// 注:webpack支持ESM和CommonJS模块的相互转换
export function square (x) {
return x * x
}
export function cube (x) {
return x * x * x
}
运行npm run build后,我们发现不管index.js里面的写法有什么不同,只要是CommonJS的语法,他们的结果都是一样的,即无法实现tree shaking,将无关的square函数引入到bunble.js中。
index.js中两种写法都是一样的结果
ok,以上三种引用方式,进一步说明了ESM和CommonJS的区别。即区别一二指出的
另外,实现tree shaking的好处就不言而喻了,可以极大减少build后js的大小。
tree shaking的实现依赖于ESM的静态分析能力,import和export可以实现tree shaking,但是直接export default 整个对象或者使用CommonJS的语法是无法实现的。
区别三
前者输出的是值的引用,后者输出的是值的拷贝。这个其实在阮一峰老师关于Module的加载实现里面有谈过这个。这边我觉得这种模块之间的引入,一般很少会说去改变模块引入进来的内容。所以具体的例子可以参考阮一峰老师的。传送门:Module 的加载实现
区别四
由于前者属于编译时加载,无法像后者一般,做到运行时加载。所以,有一个提案,引入import()函数,完成运行时加载,或者叫动态加载。import()和require()相同点都是运行时加载;区别在于,import()属于异步加载,require()属于同步加载。
运行时加载的好处:按需加载;条件加载;动态的模块路径。
什么叫运行时加载呢?就是说 你可以在一些比如条件判断语句里引入模块。而编译时加载就不行。如果使用import或者export时会报错,会告诉你这两个关键字的使用必须是在顶层的作用域才行。
if(type===1){
constmath=require('./math')
}// 报错,错误类似于// 'import' and 'export' may only appear at the top levelif(type===1){importmathfrom'./math'}
为了解决ESM中无法实现运行时加载的问题,ESM提出了import()函数的概念,来完成运行时加载。而且从例子中,我们也可以明显的看出,import函数是异步加载的,而CommonJS是同步加载的。
if(type===1){
import('./math.js').then(res=>{
// do something
}).catch(err=>{
// oops!
})
}
if(type===1){
const math=require('./math')
// do something
}