前言
模块化就是将复杂的系统分解成高内聚,低耦合的模块,让系统开发变的更加易于管理、可测试和可维护,以及解决了出现全局污染的问题。
<!-- 全局污染 -->
<script type="text/javascript">
let a = 1;
</script>
<script type="text/javascript">
a = 2;
</script>
初代模块化
为了解决全局的问题,借助了函数作用域的语言特性,和立即执行函数 IIFE 进行封装,通过“私有变量”的特性构建局部变量,由返回函数将其形成闭包保留,这种创建模块的方式成为模块模式。代表有 YUI,jQuery 等。
var Foo = (function() {
var count = 1;
return {
addCount: () => {
count += 1;
},
getCount: () => {
return count;
},
};
})();
Foo.addCount();
console.log(Foo.getCount());
也可以通过立即执行函数的参数作为不同“模块”之间的交互
var Foo = (function(Fn) {
var count = 1;
var result = Fn(count);
return {
getResult: () => {
return count;
},
};
})(handle); // 传入参数
可以从上述看出,模块模式并没有解决依赖关系,需要开发者自己关注依赖顺序和依赖管理,这对于大型应用是非常不友好的,还有个问题就是,不能很方便的访问内部属性(只能通过闭包读取)
为了解决这两个问题,于是 JS 出现了两个标准
- AMD:主要是服务于浏览器,实现有 RequireJS
- CommonJS:面向通用 JAvaScript 环境(如 Node 服务端)
AMD
AMD(Asynchronous Module Definition,异步模块定义)采用了异步加载模块的方式,当其加载完毕后,通过回调的形式,执行依赖该模块的语句。
- 自动处理依赖,无需关系依赖顺序
- 异步加载,没有阻塞
- 在一个文件内可以定义多个模块
/**
* define(id?, dependencies?, factory);
* - id: 新创建模块的ID
* - dependencies: 当前模块依赖的模块ID列表
* - factory: 依赖模块列表工厂初始化的结果作为参
*/
define('Module1', ['jQuery'], $ => {
const fn = () => {
// do something
}
return {
onClick: () => {
$(document).on('click', fn);
}
}
}
CommonJS
CommonJS 主要是服务于服务端的,所以不需要考虑浏览器网络加载的问题(该模块语法也不能在浏览器直接运行),其实现是同步声明依赖的模块定义。
Node 模块系统借鉴了 CommonJS,但是还是有一些自己的改动
- 语法简单
- 同步执行
- 以文件为单位作为模块,不能在一个文件内声明多个模块
const path = require('path');
module.exports = {
getResolvePath: (path) => path.resolve(__dirname, path);
};
为了解决 AMD 和 CommonJS 环境不同的兼容性问题,又衍生出了一个通用方案 ———— UMD(Universal Module Definition,通用模块定义)。 UMD
ES Modules
鉴于迫切的的需求,ECMAScript 标准终于提出了一个新的模块标准————ESM。其结合了 CommonJS 和 AMD 的优点,简易的模块语法,基于文件(一个文件就是一个模块)以及支持异步加载,当然它支持两个环境的执行。
语法
含义 | 写法 |
---|---|
导出变量 | export const STATE = true; |
导出函数 | export const Foo = () => {}; OR export function Foo() {}; |
导出默认 | export default Foo; |
导出多个变量 | export { Foo, STATE }; |
使用别名导出 | export { Foo as MyFoo }; |
导入多个变量 | import { Foo, STATE } from './xxx.js'; |
导入全部内容 | import * as xxx from './xxx.js'; |
别名导入变量 | import { MyFoo as Foo } from './xxx.js'; |
工作原理
- 获取模块文件:通过URL找到模块文件
<script src="main.js" type="module">
- 解析依赖,生成模块映射:根据模块文件中的import导入标识符,维护一个ModuleMap来管理依赖及其状态,如果是正在请求,就会打上fetching的标识,如果请求完成就会生成Module Record (加载都不会去阻塞主线程,因为都不会直接执行)
- 实例化:将所有匹配对应的导出和导入链接到同一个内存位置(Live Binding)
- 评估(赋值):执行顶层代码计算导出内容,并将其值放入(导入导出指向的)内存地址
export { test }
function test () {]
ESM 和 CommonJS 的区别
导出值处理
CommonJS 是值拷贝(类似函数传参),而 ESM 是值引用,即导出的内容会根据模块内的变化而变化
这是因为 CommonJS 导出时的值和后面改变的值的内存地址不是同一个,如果导出的是对象,那么地址存储不是同一个,但是指向的内存数据是同一个,所以对象属性改变,也会改变,等同于浅拷贝。
而 ESM 使用的是实时绑定(Live Binding),即导出模块的内容和导入是指向一个内存地址,当模块内值发生改变时,直接修改的是其地址的内容,而不是新的内存地址
// b.js
let state = true;
module.exports = {
state,
};
state = false;
// a.js
const b = require('./b');
console.log(b.state); // true
// b.js
export let state = true;
state = false;
// a.js
import * as b from './b';
console.log(b.state); // false
循环依赖
COMMONJS是同步加载的,代码执行遇到什么依赖就先执行依赖里的内容,并直接拿取依赖的当前浅拷贝的值
// a.js
const b = require('./b.js') //【1】当遇到依赖时,先执行依赖内容
console.log(b.state) //【5】拿到导出值,输出2
module.exports = { state: 1 } //【6】导出值,程序结束
// b.js
const a = require('./a.js') //【2】发现是循环依赖,就不在执行,直接拿到该依赖导出(浅拷贝)的值
console.log(a.state) //【3】执行导出值下的属性,输出undefined
module.exports = { state: 2 } //【4】导出值,依赖执行完成,回到a.js继续执行
// 执行a.js
// undefined
// 2
ESM是异步加载的,它首先会静态分析将代码变量提升,和将所有的依赖存入Module Map(依赖映射),映射值是该依赖的导出值引用,因此如果依赖导出值发生变更,依赖映射表也会“同步更新”
// a.mjs
import { b } from './b.js'; // 【2】当遇到依赖时,先存入依赖映射,然后执行依赖内容
console.log(b); // 【7】从依赖映射拿到引用值,输出2
var a = 1; //【1】解析遇到了var变量声明或者function函数声明,就会声明提升,但是不会赋值【8】赋值
export { a }; // 【9】导出值,程序结束
// b.js
import { a } from './a.mjs'; // 【3】发现是循环依赖,直接使用依赖映射的引用值
console.log(a); //【4】因为该变量在a文件解析中被变量提升了,所以输出undefined,如果使用const等没有变量提升,那么就会报没有初始化的错
var b = 2; // 【5】赋值
export { b }; // 【6】依赖映射值改变,依赖执行完成,回到a.js继续执行
<script type="module" src="./a.mjs"></script>
// 执行a.js
// undefined
// 2
加载方式
CommonJS 是动态加载,而 ESM 是静态加载
动态加载就是当运行时才能知道引用的模块,输出的是对象,值会被缓存
require(`${module}`);
静态加载是在编译时处理,ESM 有一个模块依赖的解析阶段(静态分析),因此可以进行依赖分析,找到没有被使用的代码,这也是能够实现 Tree Shaking 的主要原因
ES2020 支持 import 动态加载,返回一个 Promise
if (flags) {
import(`${module}`).then();
}
只读
ESM 的 import read-only 特性
import * as b from './b';
b = 1; // Error: "b" is read-only.
提升
ESM 存在 export/import 提升
console.log(b.state); // false
import * as b from './b';
浏览器支持
<script type="module" src="./a.mjs"></script>
<script type="module" src="./b.mjs"></script>
// a.mjs
import * as b from './b.mjs';
console.log(b.state);
// a.mjs
export let state = true;
state = false;
.mjs拓展名是为了更清晰的分辨是模块,也可以使用.js后缀
参考
- 《模块化系列》彻底理清 AMD,CommonJS,CMD,UMD,ES6
- Nodejs 模块加载与 ES6 模块加载实现
- JavaScript 模块的循环加载
- 《JavaScript 高级程序设计》(第 4 版)
- 《JavaScript 忍者秘籍》(第 2 版)
- 《深入理解 ES6》
- ES modules: A cartoon deep-dive