ES Module 原理详解

一、为什么会出现 ES Module?

在 ES6 之前,JS 语言本身 没有模块系统,不同环境出现了不同的实现:

  • 浏览器:没有模块机制,只能 <script> 夹杂加载

  • Node:使用 CommonJS(require)

  • 打包工具(webpack/rollup)模拟模块系统

为了统一,ES6(2015)引入了官方标准模块系统:

👉 ES Module(ESM)

它是浏览器、Node、构建工具共同支持的官方模块标准。


二、ESM 的三大特点

1. 静态解析(Static Analysis)

在编译阶段就能确定依赖关系:

javascript 复制代码
import { a } from './x.js' 
import y from './y.js'

与 CommonJS 不同,ESM 的 import 必须写在顶层,不能写成动态的:

❌ 不允许:

javascript 复制代码
if (flag) {
  import x from './xxx.js' // 错误
}

好处:

  • 可做静态分析

  • 可做 tree-shaking

  • 可做按需加载

  • 能让浏览器实现"按模块加载"


2. Live Binding(实时绑定)

可以简单理解为:

import 导入的变量不是值的拷贝,是一个"引用",始终与源模块保持同步。

示例:

javascript 复制代码
// a.js
export let count = 0
export function inc() { count++ }
javascript 复制代码
// b.js
import { count, inc } from './a.js'

console.log(count) // 0
inc()
console.log(count) // 1(会实时更新)

这就是 ES Module 的"活绑定"。


3. 模块默认是严格模式

等同于内部声明:

javascript 复制代码
'use strict'

严格模式:

  • 禁止未声明变量

  • 禁止 this 指向 window

  • 提高性能(部分优化)


三、ES Module 加载原理

浏览器原生 ESM 加载流程:

javascript 复制代码
<script type="module" src="main.js"></script>

浏览器做的事情:

1. 解析 main.js,构建依赖图(不执行)

浏览器扫描所有的 import:

javascript 复制代码
main.js
 ├── import A from './a.js'
 ├── import B from './b.js'
     └── import C from './c.js'

所有依赖会被递归扫描(静态分析)。


2. 加载所有依赖资源(并行)

浏览器会并发请求:

  • a.js

  • b.js

  • c.js

不像 CommonJS 是"边执行边加载"。


3. 将所有模块放入 Module Map(模块缓存)

浏览器维护一个模块缓存(Module Map):

javascript 复制代码
URL → 该模块的导出对象

每个模块只执行一次。


4. 执行模块(自上而下)

模块执行顺序符合依赖图拓扑排序。


四、Node.js 中的 ES Module 原理

Node 原生支持 ESM,有两种方式:

方式 1:使用 .mjs 后缀

javascript 复制代码
import fs from 'fs'

方式 2:在 package.json 加:

javascript 复制代码
{ "type": "module" }

Node 加载 ESM 的主要特点:

  • 不允许 import 动态使用(与浏览器一致)

  • 使用 URL 解析(必须加文件后缀)

  • CommonJS 中的 require 机制不同(不是同一个系统)


五、ESM 与 CommonJS 对比(核心差异)

特性 ES Module CommonJS
加载方式 编译时确定依赖,静态导入 运行时执行 require,动态导入
导出内容 Live Binding 值拷贝(执行时)
支持 tree-shaking ✔ 支持 ❌ 不支持
是否异步 ✔(浏览器) ❌ 同步
使用环境 浏览器、Node Node
加载顺序 依赖图拓扑排序 require 执行顺序

静态 vs 动态 本质区别

ESM 是静态的,CommonJS 是动态的。


六、为什么 ESM 对构建工具非常重要?

(1)可以静态分析依赖树

Vite、Rollup、Webpack 都可以根据 import 解析依赖,不用真正执行 JS。

(2)可以做 tree-shaking

编译时就能知道哪些变量没用,直接删掉。

(3)Vite 可以实现"按需编译"

因为浏览器能的直接请求 ESM:

javascript 复制代码
import { x } from '/src/utils/helper.js'

Vite 会对这个模块即时编译,而不是打包后再运行。

👉 这就是 Vite 秒启动的核心原理!


七、浏览器 ESM 的高阶特性

1. 模块的跨域加载

script module 支持跨域加载,只要正确 CORS:

javascript 复制代码
<script type="module" src="https://example.com/main.js"></script>

2. 动态 import(支持异步、懒加载)

ESM 的动态加载:

javascript 复制代码
import('./feature.js').then(m => {
  m.run()
})

这个语法在浏览器、Node 都支持。


3. 模块只能执行一次

无论 import 多少次,结果来自同一份 Module Map 缓存。


八、ES Module 执行顺序

当浏览器遇到:

javascript 复制代码
<script type="module" src="./main.js"></script>

整体加载流程如下:

bash 复制代码
 ┌─────────────────────────────────┐
 │ 1. 浏览器下载 main.js           │
 └───────────────┬───────────────┘
                 ▼
       ┌───────────────────┐
       │ 2. 解析依赖 (静态) │◄───────────────────────────────────────────┐
       └──────────┬────────┘                                            │
                  │                                                     │
     ┌────────────┴────────────┐                                       │
     ▼                           ▼                                      │
┌─────────────┐        ┌─────────────────┐                              │
│ import A.js │        │ import B.js     │                              │
└───────┬─────┘        └─────────┬──────┘                              │
        ▼                         ▼                                     │
   ┌─────────────┐         ┌─────────────┐                               │
   │ 下载 A.js    │         │ 下载 B.js    │                               │
   └───────┬─────┘         └───────┬─────┘                               │
           ▼                        ▼                                    │
┌────────────────────┐    ┌────────────────────┐                          │
│ 解析 A.js 的 import │    │ 解析 B.js 的 import │                          │
└──────────┬─────────┘    └──────────┬─────────┘                          │
           │                          │                                    │
     (如果有子依赖)                 (如果有子依赖)                          │
           ▼                          ▼                                    │
    ┌────────────────┐       ┌────────────────┐                           │
    │ 递归加载依赖树 │──────►│ C.js / D.js ...│                           │
    └────────────────┘       └────────────────┘                           │
                  │                                                     │
                  └───────────────► 完成完整依赖图构建 ◄────────────────┘
                                     (Module Graph 完成)
                                     (不执行 JS)
─────────────────────────────────────────────────────────────────────────────
接下来进入执行阶段(按依赖拓扑顺序执行)
─────────────────────────────────────────────────────────────────────────────

示例:

javascript 复制代码
console.log('main start') 
import './a.js' 
console.log('main end')

执行顺序:

  1. 浏览器静态扫描 main → 找到 a.js

  2. 浏览器并行加载 a.js

  3. 执行 a.js

  4. 最后执行 main.js 的代码

说明:import 会提前加载依赖模块,而不是等执行到那一行才加载。


九、ES Module 总结一句话

ES Module 是一种"静态依赖分析 + 原生 ESM 加载 + 模块之间实时绑定"的现代模块系统,是浏览器、Node、Vite 等工具的底层基础。

静态 → 能做 Tree-shaking

实时绑定 → 执行效率高

可缓存 → 浏览器运行快

原生支持 → Vite 可以不打包直接跑

相关推荐
小满zs1 小时前
Next.js第八章(路由处理程序)
前端
冴羽1 小时前
Cloudflare 崩溃梗图
前端·javascript·vue.js
Jonathan Star2 小时前
JavaScript 中,原型链的**最顶端(终极原型)只有一个——`Object.prototype`
开发语言·javascript·原型模式
鹿衔`2 小时前
解决Flink on Yarn模式多Yarn Session会话提交
java·前端·flink
u***u6853 小时前
前端组件单元测试模拟,Jest mock函数
前端·单元测试
前端摸鱼匠3 小时前
Vue 3 的watchEffect函数:介绍watchEffect的基本用法和特点
前端·javascript·vue.js·前端框架·ecmascript
拉不动的猪3 小时前
基本数据类型Symbol的基本应用场景
前端·javascript·面试
天庭鸡腿哥3 小时前
谷歌出品,堪称手机版PS!
javascript·智能手机·eclipse·maven
_小九4 小时前
【开源】耗时数月、我开发了一款功能全面【30W行代码】的AI图床
前端·后端·开源