认识
一、认识
ES6 Module
也被称作 ES Module
(或 ESM
), 是由 ECMAScript
官方提出的模块化规范,作为一个官方提出的规范,ES Module
经过五年多的发展,不仅得到了众多浏览器的原生支持,也在 Node.js
中得到了原生支持,是一个能够跨平台的模块规范。。在现代浏览器中,如果在 HTML
中加入含有type="module"
属性的 script
标签,那么浏览器会按照 ES Module
规范来进行依赖加载和模块解析。EsModule
通过 export
关键字导出模块中的功能,可以是变量、函数、类等。使用 import
关键字导入其他模块的功能。
Es Module
加载机制: 通过 import
语句静态分析、导入, 也就是说在编译时就完成加载。也可以通过 import()
函数进行异步、动态导入,允许按需加载模块。
Es Module
加载时机: ES Module
是静态编译, 即编译时加载, 也就是说在编译时就完成加载, 因此在编译时就能够确定模块的依赖关系, 输入和输出变量。意味着在代码执行之前, 需要对模块结构进行分析, 所以 import
语句必须位于模块最顶层。Es Module
同样支持 import()
动态导入, 异步加载模块, 返回一个 Promise
。
Es Module
缓存机制: ES Module
使用 URL
或者相对路径作为模块标识。每个模块的加载是基于其 URL
的唯一性。当一个模块通过 import
语句被加载时,浏览器(或 JavaScript
运行时环境)会解析其 URL
,并将模块内容缓存起来。这个缓存是基于模块的 URL
的。模块的缓存是通过 URL
进行标识和管理的。如果一个模块的 URL
相同,则表示它是同一个模块,浏览器会重用之前缓存的实例,而不是重新加载和执行模块代码。包括通过动态导入语法 import()
,可以异步加载模块。这种动态加载也会受到缓存机制的影响,即相同的 URL
会返回缓存中的模块实例。
Es Module
导出结果: ES Module
模块导入的实例都是单例的, 当多个模块导入同一个模块时,所有导入都引用相同的模块实例。模块的内容(例如导出的变量、函数、对象等)在整个应用中只有一个实例。修改模块的内容(如变量、对象属性等)会影响所有引用该模块的地方,因为它们引用的是同一个实例。综上所述: ES Modules
确保了导出的模块在整个应用中只有一个实例。所有对该模块的引用都共享同一个实例,确保状态的一致性。
Es Module Tree shaking
是基于 ES6
模板语法(import
与exports
),主要是借助ES6
模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量, 进而判断哪些模块已经加载, 哪些模块和变量未被使用或者引用,进而删除对应代码。
二、语法
2.1 基础语法
定义模块
// 命名导出
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// 默认导出
export default function multiply(a, b) {
return a * b;
}
使用模块
// 导入命名导出
import { add, subtract } from './math';
// 导入默认导出
import multiply from './math';
console.log(add(1, 2)); // 3
console.log(subtract(5, 3)); // 2
console.log(multiply(2, 3)); // 6
// 动态导入
import('./math').then(module => {
console.log(module.add(1, 2)); // 3
});
2.2 导出结果
Es Module
导出结果: ES Module
模块导入的实例都是单例的, 当多个模块导入同一个模块时,所有导入都引用相同的模块实例。模块的内容(例如导出的变量、函数、对象等)在整个应用中只有一个实例。修改模块的内容(如变量、对象属性等)会影响所有引用该模块的地方,因为它们引用的是同一个实例。综上所述: ES Modules
确保了导出的模块在整个应用中只有一个实例。所有对该模块的引用都共享同一个实例,确保状态的一致性。
一、验证a.js
、c.js
导入 b.js
, a.js
修改 b.js
中的变量,c.js
是否有影响?:
// b.js
export let e = 1;
import { e } from './d.js';
e = 5;
console.log(e); // 输出: 5
// c.js
import { e } from './d.js';
console.log(e); // 输出: 5
-
a.js
、c.js
都导入了b.js
模块中的e
变量。 -
a.js
修改了e
的值。 -
因为
e
是b.js
中单一实例的引用,c.js
中也会反映a.js
的修改。 -
结果是
c.js
中的e
变量的值是5
。
二、验证a.js
、c.js
导入 b.js
, a.js
修改 b.js
中的对象属性值,c.js
是否有影响?:
// b.js
export let e = { value: 1 };
import { e } from './d.js';
e.value = 5;
console.log(e.value); // 输出: 5
// c.js
import { e } from './d.js';
console.log(e.value); // 输出: 5
-
a.js
、c.js
都导入了b.js
模块中的e
对象属性值。 -
a.js
修改了e
的对象属性值。 -
因为
e
是b.js
中单一实例的引用,c.js
中也会反映a.js
的修改。 -
结果是
c.js
中的e.value
变量的值是5
。
三、场景
3.1 Node
3.2 Webpack
3.3 HTML module src
<script type="module" src="./a.js">
b();
</script>
3.4 HTML module from
<script type="module">
import { b } from './b.js';
b();
</script>
3.5 HTML module importmap
importmap
是Chrome 89
才支持的。它是对import
的一个映射处理,让你控制在js
中使用import
时,到底从哪个url
获取这些库。
<script type="importmap">
{
"imports": {
"B": "./b.js"
}
}
</script>
<script type="module">
import { b } from 'B';
b();
</script>
四、问题
4.1 EsModule 会发生循环引用吗? 如何解决?
EsModule
循环引用的场景如下:
// a.js
import { funcB } from './b.js';
funcB();
export var funcA = () => {
console.log('a');
}
// b.js
import { funcA } from './a.js';
funcA();
export var funcB = () => {
console.log('b')
}
接着我们可以执行一下 a.js
文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script type="module" src="/a.js"></script>
</body>
</html>
在浏览器中打开会出现类似的报错:
代码的执行原理如下:
-
JS
引擎执行a.js
时,发现引入了b.js
,于是去执行b.js
-
引擎执行
b.js
,发现里面引入了a.js
(出现循环引用),认为a.js
已经加载完成,继续往下执行 -
执行到
funcA()
语句时发现funcA
并没有定义,于是报错。
4.2 CommonJs 与 ES Modules 有什么区别?
CommonJS
使用 module.exports
或 exports
对象来暴露模块的功能。使用 require
函数来导入和使用其他模块。
-
CommonJS
加载机制: 通过require
函数同步加载,即模块加载完成后代码才会继续执行。这种方式适合服务器端环境,但在浏览器中可能会导致性能问题。CommonJS
本身约定以同步的方式进行模块加载,这种加载机制放在服务端是没问题的,一来模块都在本地,不需要进行网络IO
,二来只有服务启动时才会加载模块,而服务通常启动后会一直运行,所以对服务的性能并没有太大的影响。但如果这种加载机制放到浏览器端,会带来明显的性能问题。它会产生大量同步的模块请求,浏览器要等待响应返回后才能继续解析模块。也就是说,模块请求会造成浏览器JS
解析过程的阻塞,导致页面加载速度缓慢。 -
CommonJS
加载时机:CommonJS
在代码运行时加载模块, 可以通过require
实现动态、同步加载模块。不支持静态分析, 没有办法在编译时进行优化。因此,CommonJS
允许require
可以在任何地方动态的加载模块。 -
CommonJS
缓存机制: 当模块第一次被加载时,Node.js
会将其缓存起来。后续对相同模块的require
调用将返回缓存中的实例,而不会重新执行模块代码。这确保了模块的单例特性和一致性。 -
CommonJS
导出结果: 原始值(字符串、数字、布尔值) 是不可变的,CommonJS
中导出的原始值是拷贝的, 其他模块通过require
获取的是对这个原始值的拷贝。修改模块内部的原始值不会影响到其他地方,因为其他地方获得的是原始值的拷贝。对象和函数是可变的,CommonJS
中导出的对象或函数是引用的, 其他模块通过require
获取的是对这个对象或函数的引用。修改模块内部的对象或函数的状态会影响所有引用它的模块,因为这些模块共享同一个实例。
EsModule
通过 export
关键字导出模块中的功能,可以是变量、函数、类等。使用 import
关键字导入其他模块的功能。
-
Es Module
加载机制: 通过import
语句静态分析、导入, 也就是说在编译时就完成加载。也可以通过import()
函数进行异步、动态导入,允许按需加载模块。 -
Es Module
加载时机:ES Module
是静态编译, 即编译时加载, 也就是说在编译时就完成加载, 因此在编译时就能够确定模块的依赖关系, 输入和输出变量。意味着在代码执行之前, 需要对模块结构进行分析, 所以import
语句必须位于模块最顶层。Es Module
同样支持import()
动态导入, 异步加载模块, 返回一个Promise
。 -
Es Module
缓存机制:ES Module
使用URL
或者相对路径作为模块标识。每个模块的加载是基于其URL
的唯一性。当一个模块通过import
语句被加载时,浏览器(或JavaScript
运行时环境)会解析其URL
,并将模块内容缓存起来。这个缓存是基于模块的URL
的。模块的缓存是通过URL
进行标识和管理的。如果一个模块的URL
相同,则表示它是同一个模块,浏览器会重用之前缓存的实例,而不是重新加载和执行模块代码。包括通过动态导入语法import()
,可以异步加载模块。这种动态加载也会受到缓存机制的影响,即相同的URL
会返回缓存中的模块实例。 -
Es Module
导出结果*:ES Module
模块导入的实例都是单例的, 当多个模块导入同一个模块时,所有导入都引用相同的模块实例。模块的内容(例如导出的变量、函数、对象等)在整个应用中只有一个实例。修改模块的内容(如变量、对象属性等)会影响所有引用该模块的地方,因为它们引用的是同一个实例。综上所述:ES Modules
确保了导出的模块在整个应用中只有一个实例。所有对该模块的引用都共享同一个实例,确保状态的一致性。 -
Es Module Tree shaking
是基于ES6
模板语法(import
与exports
),主要是借助ES6
模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量, 进而判断哪些模块已经加载, 哪些模块和变量未被使用或者引用,进而删除对应代码。
4.3 CommonJs 与 ES Modules 可以相互引用吗?
在ES Modules
模块当中,是支持加载CommonJS
模块 的。但是反过来,CommonJS
并不能require
ES Modules
模块
4.4 为什么 Es Modules 模块比 CommonJS 模块更好?
ES Modules
(ESM
)相较于 CommonJS
模块具有多方面的优势。这些优势主要体现在标准化、性能优化、静态分析等方面。下面详细介绍 ES Modules
的这些优点以及它们如何使 ESM
相较于 CommonJS
更好。
1. 标准化和一致性: ES Modules
是 JavaScript
的官方模块标准,自 ECMAScript 2015
(ES6
)起被引入。作为官方标准,它具有以下优点:
-
统一的语法:
ESM
提供了一套统一的语法(import
和export
),消除了不同模块系统之间的语法不一致性。 -
广泛支持:所有现代
JavaScript
环境(包括浏览器和Node.js
)都支持ES Modules
。随着时间的推移,ESM
成为JavaScript
模块化的标准选择。
2. 静态分析和优化: ES Modules
支持静态分析,使得编译工具和打包工具能够进行更高效的优化:
-
静态结构:
ESM
使用静态的import
和export
语法,这使得模块的依赖关系在编译时就可以被解析。编译工具可以在编译时确定哪些模块被导入,哪些被导出。 -
树摇(
Tree Shaking
):由于静态结构,工具可以执行树摇优化,去除未使用的代码,从而减小打包后的文件体积。
3. 异步加载: ES Modules
支持异步模块加载,提供了动态 import()
语法, 动态导入, 可以按需加载模块,从而优化应用的加载时间和性能。动态导入使得模块可以在运行时进行加载,允许更灵活的代码拆分。
4. 更好的性能: ESM
使用 URL
作为模块标识,模块加载和缓存机制更高效。每个模块只加载一次,所有导入都共享同一个实例。由于静态分析支持,现代打包工具能够对 ES Module
进行更多优化,如代码分割和按需加载,从而提高性能。
5. 更强的语法支持: ESM
允许模块同时使用命名导出和默认导出,提供了更灵活的导出方式。ESM
支持在模块顶层直接使用 await
,使得异步编程更加直观。