跳到主要内容

认识

2023年03月05日
柏拉文
越努力,越幸运

一、认识


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 模板语法(importexports),主要是借助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.jsc.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.jsc.js 都导入了 b.js 模块中的 e 变量。

  • a.js 修改了 e 的值。

  • 因为 eb.js 中单一实例的引用,c.js 中也会反映 a.js 的修改。

  • 结果是 c.js 中的 e 变量的值是 5

二、验证a.jsc.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.jsc.js 都导入了 b.js 模块中的 e 对象属性值。

  • a.js 修改了 e 的对象属性值。

  • 因为 eb.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

importmapChrome 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>

在浏览器中打开会出现类似的报错:

Preview

代码的执行原理如下:

  1. JS 引擎执行 a.js 时,发现引入了 b.js,于是去执行 b.js

  2. 引擎执行b.js,发现里面引入了a.js(出现循环引用),认为a.js已经加载完成,继续往下执行

  3. 执行到funcA()语句时发现 funcA 并没有定义,于是报错。

Preview

4.2 CommonJs 与 ES Modules 有什么区别?

CommonJS 使用 module.exportsexports 对象来暴露模块的功能。使用 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 模板语法(importexports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量, 进而判断哪些模块已经加载, 哪些模块和变量未被使用或者引用,进而删除对应代码。

4.3 CommonJs 与 ES Modules 可以相互引用吗?

ES Modules模块当中,是支持加载CommonJS模块 的。但是反过来,CommonJS 并不能require ES Modules 模块

4.4 为什么 Es Modules 模块比 CommonJS 模块更好?

ES ModulesESM)相较于 CommonJS 模块具有多方面的优势。这些优势主要体现在标准化、性能优化、静态分析等方面。下面详细介绍 ES Modules 的这些优点以及它们如何使 ESM 相较于 CommonJS 更好。

1. 标准化和一致性: ES ModulesJavaScript 的官方模块标准,自 ECMAScript 2015ES6)起被引入。作为官方标准,它具有以下优点:

  • 统一的语法:ESM 提供了一套统一的语法(importexport),消除了不同模块系统之间的语法不一致性。

  • 广泛支持:所有现代 JavaScript 环境(包括浏览器和 Node.js)都支持 ES Modules。随着时间的推移,ESM 成为 JavaScript 模块化的标准选择。

2. 静态分析和优化: ES Modules 支持静态分析,使得编译工具和打包工具能够进行更高效的优化:

  1. 静态结构:ESM 使用静态的 importexport 语法,这使得模块的依赖关系在编译时就可以被解析。编译工具可以在编译时确定哪些模块被导入,哪些被导出。

  2. 树摇(Tree Shaking):由于静态结构,工具可以执行树摇优化,去除未使用的代码,从而减小打包后的文件体积。

3. 异步加载: ES Modules 支持异步模块加载,提供了动态 import() 语法, 动态导入, 可以按需加载模块,从而优化应用的加载时间和性能。动态导入使得模块可以在运行时进行加载,允许更灵活的代码拆分。

4. 更好的性能: ESM 使用 URL 作为模块标识,模块加载和缓存机制更高效。每个模块只加载一次,所有导入都共享同一个实例。由于静态分析支持,现代打包工具能够对 ES Module 进行更多优化,如代码分割和按需加载,从而提高性能。

5. 更强的语法支持: ESM 允许模块同时使用命名导出和默认导出,提供了更灵活的导出方式。ESM 支持在模块顶层直接使用 await,使得异步编程更加直观。