‌ES Module 都过十岁生日了,你还不了解它的运行原理吗?

前言

Hello~大家好。我是秋天的一阵风

作为前端工程师,我们每天都在用 importexport,但你知道它们背后到底发生了什么吗?本文将带你深入了解 ES Module 的运行机制,探索浏览器中模块加载的细节。别眨眼,干货满满!

Tips:

如果你对CommonJS感兴趣,还可以阅读我的这篇文章: 一道Commonjs的笔试题,让我彻底搞懂require机制!

一、什么是ES Module ?

ES ModuleECMAScript(JavaScript 的标准)中用于模块化代码的规范。它是 JavaScript 原生支持的模块化语法,旨在解决代码组织、复用和依赖管理的问题。ES Module 最初在 ECMAScript 2015(ES6)中引入,并在后续版本中不断完善。

1. ES Module 的特点

1.1 原生支持

ES Module 是 JavaScript 语言标准的一部分,由浏览器和 Node.js 原生支持,无需额外的构建工具(如 Webpack 或 Babel)即可使用。

1.2 静态导入

ES Module 使用静态导入(import)和导出(export)语法,这意味着模块的依赖关系在代码加载时就已经确定。这与 CommonJS(Node.js 中的模块系统)的动态 require 不同,使得工具(如 Webpack)能够进行静态分析和优化。

1.3 单例模式

每个模块文件被视为一个独立的作用域,模块中的变量和函数不会污染全局作用域。模块的导出内容是单例的,即多次导入同一个模块时,返回的是同一个实例。

1.4 支持异步加载

ES Module 支持动态导入(import()),返回一个 Promise,可用于按需加载模块,优化应用性能。

2. ES Module 的语法

2.1 导出(Export)

在模块中,可以使用 export 关键字导出变量、函数或类。

JavaScript 复制代码
// myModule.js
export const name = "Kimi";
export function sayHello() {
    console.log("Hello, I'm Kimi!");
}
export class Animal {
    constructor(name) {
        this.name = name;
    }
}

也可以使用默认导出(default),每个模块只能有一个默认导出。

JavaScript 复制代码
// myModule.js
const name = "Kimi";
function sayHello() {
    console.log("Hello, I'm Kimi!");
}

export default { name, sayHello };

2.2 导入(Import)

在其他模块中,可以使用 import 关键字导入导出的内容。

JavaScript 复制代码
// 使用命名导出
import { name, sayHello, Animal } from "./myModule.js";

console.log(name); // Kimi
sayHello(); // Hello, I'm Kimi!
const dog = new Animal("Buddy");

对于默认导出,可以使用任意名称导入:

JavaScript 复制代码
// 使用默认导出
import myModule from "./myModule.js";

console.log(myModule.name); // Kimi
myModule.sayHello(); // Hello, I'm Kimi!

2.3 动态导入

ES Module 支持动态导入,返回一个 Promise,可用于按需加载模块。

JavaScript 复制代码
// 动态导入模块
async function loadModule() {
    const myModule = await import("./myModule.js");
    myModule.sayHello(); // Hello, I'm Kimi!
}

loadModule();

二、ES Module 与 CommonJS 的区别

特性 ES Module CommonJS
导入方式 静态导入(import 动态导入(require
导出方式 命名导出和默认导出 默认导出(module.exports
单例模式
浏览器支持 原生支持 需要构建工具(如 Browserify)
Node.js 支持 v12+,需设置 "type": "module" 默认支持
性能优化 支持静态分析和树摇优化 动态加载,难以优化

三、ES Module 的运行原理

ES Module 的运行过程可以分为以下几个阶段。

1. 补全路径和下载文件

当代码中出现 import 语句时,JavaScript 引擎会首先解析模块路径,将其转换为模块的绝对路径 。这个过程与 require 类似,但 ES Module 支持更多路径格式,如相对路径(./../)和绝对路径(/)。

2. 解析文件

ES Module拿到文件以后会怎么解析文件呢?

它会先去找文件中的所有顶级静态导入语句 ,这里有两个关键词,一个是静态 ,一个是顶级

静态导入 这个非常简单,除了使用import函数导入以外的都属于静态导入,例如:

css 复制代码
import a from "./a.js";

顶级是啥意思呢,也就是写在全局中的导入语句,像if语句或者for循环里面的都不属于顶级。

我们以上面的代码案例中main.js文件来举例,

javascript 复制代码
//main.js

import a from "./a.js";

import("./d.js").then((val) => {
  console.log("main.js val:", val.default);
});
console.log("main:", a, b);

import b from "./b.js";

main.js文件中,符合顶级静态导入语句这个限定词的就只有两个导入语句:

javascript 复制代码
import a from "./a.js";
import b from "./b.js";

3. 递归处理

拿到了以上两个导入语句以后,就会递归进行进一步的解析,也就是对a.jsb.js文件,把之前的补全路径和下载文件流程再递归走一遍

4. 建立Module Map映射表

EsModule会 生成一份名为Module Map的映射,以url为key,每个url对应一份Module Record

每份Module Record记录了当前模块导入和导出的信息:

  • 导入信息:当前模块依赖的其他模块、当前模块导入的变量
  • 导出信息:当前模块导出的变量

这样做是为了对模块信息进行缓存:当某个模块a被多个模块依赖时,我们能在Module Map中找到下载模块a的url,从而使得模块a最终只被下载一次

5. 模块执行

模块加载完成后,JavaScript 引擎会执行模块代码。ES Module 的执行顺序是 按导入顺序 执行的,这意味着如果模块 A 导入了模块 B 和模块 C,那么模块 B 和模块 C 会先于模块 A 执行。

在执行过程中,模块的导出内容会被缓存,多次导入同一个模块时,返回的是同一个实例。

下面,我们通过一个案例代码中来探究执行打印顺序是怎么样的。

四、 案例代码

我们创建几个文件,其中在main.js文件中,d.js文件是通过动态导入的方式,代码和结构图如下:

index.html

javascript 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./main.js" type="module"></script>
  </head>
  <body></body>
</html>

main.js

javascript 复制代码
//main.js
import a from "./a.js";

import("./d.js").then((val) => {
  console.log("main.js val:", val.default);
});
console.log("main:", a, b);

import b from "./b.js";

a.js

javascript 复制代码
import c from "./c.js";

console.log("a:", c);

export default "a";

b.js

javascript 复制代码
console.log("b.js");

export default "b";

c.js

javascript 复制代码
console.log("c.js");
export default "c";

d.js

javascript 复制代码
console.log("d.js");
export default "d";

结构图

  • 首先main.js中的第一条导入语句会先执行 import a from "./a.js"; ,
  • a.js文件中又导入了c.js,所以会先执行c.js中的内容,所以第一个打印的就是 "c.js"
  • 回到a.js文件,执行a.js自己的打印语句,也就是 "a: c"

  • a.js文件处理完了,回到main.js文件,继续处理b.js的导入语句import b from "./b.js";
  • 进入b.js文件,执行b.js自己的打印语句,也就是 "b.js"

  • b.js文件处理完了,回到main.js文件,处理main.js自己的打印语句,也就是 "main: a b"

  • 最后会处理动态导入语句,进入到d.js,执行它的打印语句,也就是 "d.js"

  • 处理d.js动态导入回调函数中的打印语句:"main.js val:d"

控制台结果如下:

五、 循环依赖问题

1. Module Map

ES Module 使用模块地图来记录每个模块的加载状态。当模块被导入时,模块地图会标记该模块为"正在加载(Fetching)"状态。如果在加载过程中遇到循环依赖,模块地图会检查目标模块是否已经被标记为"正在加载"。如果是,则不会再次进入该模块的加载流程,从而避免了死循环。

2. Module Record

每个模块都有一个模块记录,其中包含了模块的导出变量的内存地址。当一个模块被导入时,ES Module 不会立即执行模块代码,而是先将模块记录中的导出变量绑定到当前模块的作用域中。这意味着即使在模块代码尚未完全执行时,其他模块也可以访问到这些导出变量的初始值。

3. 动态绑定

ES Module 的导出是动态绑定的,即导出的内容指向的是内存地址,而不是值的拷贝。这意味着即使在循环依赖的情况下,模块之间的变量更新也会保持同步。

虽然 ES Module 提供了一些机制来处理循环依赖,但最好的方法是通过重新设计代码来避免循环依赖。如果无法避免,可以使用动态导入、延迟执行或事件机制等技术来解决循环依赖问题

六、总结

ES Module 是 JavaScript 原生支持的模块化规范,具有 原生支持静态导入单例模式支持异步加载 等特点。ES Module 的运行原理包括 模块解析模块加载模块执行 三个阶段。

相关推荐
吞吞07112 分钟前
浅谈前端性能指标、性能监控、以及搭建性能优化体系
前端
JiangJiang3 分钟前
5 分钟掌握 TypeScript 结构化类型系统,一次搞懂鸭子类型!
javascript·面试
arcsin13 分钟前
雨水-electron项目实战登录
前端·electron·node.js
卑微小文12 分钟前
企业级IP代理安全防护:数据泄露风险的5个关键防御点
前端·后端·算法
SameX14 分钟前
HarmonyOS Next ohpm-repo私有仓库的配置与优化
前端·harmonyos
咪库咪库咪14 分钟前
async await
前端·javascript
华科云商xiao徐16 分钟前
用TypeScript和library needle来创建视频爬虫程序
前端
lovebugs16 分钟前
如何保证Redis与MySQL双写一致性?分布式场景下的终极解决方案
后端·面试
养生匠16 分钟前
我写了个yapi 转化前端ts请求接口的代码,真的很好用
javascript
hahala233317 分钟前
依赖注入(DI)
javascript·node.js