在讲这两者的区别之前我先来介绍一下为什么要学习ES Module和commonJs,这两者的最大作用就是使整个前端项目模块化,那什么是模块化呢?
百度百科解释道:模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。
那我自己的理解就是:将一个类型的东西归类到一起,比如这个类型的属性,方法归为一个文件,这样一个个文件,就形成了一个个模块。
早期的 Javascript 是没有模块化的概念,如果想利用 Javascript 构建一个大型项目,就会有很多问题。例如 1.命名冲突;2.变量私有;2.依赖关系的管理等问题。
接下来我们就来总结一下ES Modules和CommonJS的区别吧
1. 导入导出语法
两者的模块导入导出语法不同,CommonJs是通过module.exports,exports导出,require导入;ES Modules则是export导出,import导入。
// ES Modules - stores/index.ts
import { useUserStore } from './user';
import { useRouterStore } from './router';
export { useUserStore, useRouterStore };
// CommonJS - 等价写法
const { useUserStore } = require('./user');
const { useRouterStore } = require('./router');
module.exports = { useUserStore, useRouterStore };
2. 导入时机
CommonJS是运行时加载模块,ES Modules是在静态编译期间就确定模块的依赖。
// ES Modules - 编译时确定依赖关系
// recommend.vue
import { useUserStore, useRouterStore } from '@store';
import SignOverlay from '@component/SignOverlay.vue';
import LoadingPage from './components/loading.vue';
// CommonJS - 运行时加载
const stores = process.env.NODE_ENV === 'development'
? require('./store.dev')
: require('./store.prod');
ES Modules这样设计的优势体现了出来,我们来结合具体项目中的实际场景来说一下
(1)静态分析优化
将没有使用到的引用移除
// stores/index.ts
export * from './user';
export * from './router';
export * from './common';
// 使用处 recommend.vue
import { useUserStore, useRouterStore } from '@store';
// 打包工具可以分析出:
// 1. 只需要 user.ts 和 router.ts
// 2. common.ts 可以被移除
// 3. 生成更小的包体积
(2)提前发现错误
ES Modules可以在编译时就爆出错误,而CommonJS需要到运行时才能发现
// 错误的导入
import { nonExistStore } from '@store'; // 编译时报错
import { useUserStore } from './wrong/path'; // 编译时报错
// CommonJS 要到运行时才能发现
const store = require('./wrong/path'); // 运行时才报错
(3)更好的 Tree-shaking
你引入的东西如果没有使用,打包的时候会被删除,减小打包体积
// components/AuthOverlay.vue
import { UNUSE_PATHÏ } from '@constant';//在后面的代码中没有使用
// 打包工具可以:
// 1. 只打包用到的常量
// 2. 删除未使用的代码
// 3. 减小最终包体积
(4)更好的代码分割
可以在编译的时候就确定了依赖,自动分析依赖关系,实现按需加载
// pages/recommend/recommend.vue
import LoadingPage from './components/loading.vue';
import SignOverlay from '@component/SignOverlay.vue';
// 打包工具可以:
// 1. 自动分析组件依赖
// 2. 进行代码分割
// 3. 实现按需加载
3. 导出值的处理
CommonJS导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,则不会同步到外部。ES Modules是导出的一个引用,内部修改可以同步到外部。
// CommonJS - 导出值的拷贝
// module.js
let count = 0;
module.exports = {
count,
increment: () => count++
};
// main.js
const counter = require('./module');
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 0 (值不会改变)
// ES Modules - 导出值的引用
// module.js
export let count = 0;
export const increment = () => count++;
// main.js
import { count, increment } from './module';
console.log(count); // 0
increment();
console.log(count); // 1 (值会改变)
4. 循环依赖处理
主要区别:
CommonJS:模块加载是同步的、遇到循环依赖时会返回未完成的导出对象、可以正常运行,但可能得到部分未完成的值
ES Modules:模块加载是异步的、在解析阶段就会检测到循环依赖、会抛出错误,防止出现不可预期的结果
如果还是不理解 可以看一下这篇文章
https://blog.youkuaiyun.com/xgangzai/article/details/127218919?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522afcb29c08de73462e849ddae1aac1f6d%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=afcb29c08de73462e849ddae1aac1f6d&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-1-127218919-null-null.142v101pc_search_result_base4&utm_term=esmodule%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96&spm=1018.2226.3001.4187Ï
(1)CommonJS 循环依赖
//index.js
const a = require('./a.js')
// a.js
console.log('a.js 开始执行');
exports.done = false;
const b = require('./b.js');
console.log('在 a.js 中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
// b.js
console.log('b.js 开始执行');
exports.done = false;
const a = require('./a.js');
console.log('在 b.js 中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
// 执行结果:
// a.js 开始执行
// b.js 开始执行
// 在 b.js 中,a.done = false // 得到的是未完成的 a.js 的导出值
// b.js 执行完毕
// 在 a.js 中,b.done = true
// a.js 执行完毕
(2)ES Module循环依赖
//index.mjs
import { done as aDone } from './a.mjs'
// a.mjs
console.log('1. 开始执行 a.mjs'); // 第1步:打印 "1. 开始执行 a.mjs"
export let done = false; // 第2步:声明并初始化 a.mjs 的 done 变量为 false
import { done as bDone } from './b.mjs'; // 第3步:暂停执行 a.mjs,开始执行 b.mjs
console.log('4. 在 a.mjs 中, bDone =', bDone); // 第7步:打印 "4. 在 a.mjs 中, bDone = true"
done = true; // 第8步:修改 a.mjs 的 done 为 true
console.log('5. a.mjs 执行完毕'); // 第9步:打印 "5. a.mjs 执行完毕"
// b.mjs
console.log('2. 开始执行 b.mjs'); // 第4步:打印 "2. 开始执行 b.mjs"
export let done = false; // 第5步:声明并初始化 b.mjs 的 done 变量为 false
import { done as aDone } from './a.mjs'; // 第6步:尝试访问 a.mjs 的 done
// 错误:此时 a.mjs 的 done 还在 TDZ 中
console.log('3. 在 b.mjs 中, aDone =', aDone); // 不会执行到这里,因为上一步已经抛出错误
done = true; // 不会执行
console.log('b.mjs 执行完毕'); // 不会执行
// 实际执行结果:
// 1. 开始执行 a.mjs
// 2. 开始执行 b.mjs
// ReferenceError: Cannot access 'done' before initialization
5. this 指向
CommonJS中顶层的this指向这个模块本身,而ES Modules中顶层this指向undefined。
// CommonJS
console.log(this === module.exports); // true
// ES Modules
console.log(this); // undefined
6. 加载内容
CommonJS加载的是整个模块,将所有的接口全部加载进来,ES Modules可以单独加载其中的某个接口;
// stores/common.ts
module.exports = {
state: { /*...*/ },
mutations: { /*...*/ },
actions: { /*...*/ },
getters: { /*...*/ }
};
// 使用时必须加载整个模块
const commonStore = require('@store/common');
// 即使只需要 state,也会加载整个模块的所有内容
const { state } = commonStore;
// stores/common.ts
export const state = { /*...*/ };
export const mutations = { /*...*/ };
export const actions = { /*...*/ };
export const getters = { /*...*/ };
// 使用时可以只加载需要的部分
import { state } from '@store/common'; // 只加载 state
import { actions } from '@store/common'; // 只加载 actions