Skip to content

ES Modules (ESM) 的整个工作流程

想象一下,你正在建造一个大型的乐高模型,这个模型由许多小的、独立的部分组成。ES Modules 就好比是这个模型的说明书和零件分类系统。它能帮助浏览器(或者 Node.js 环境)高效、有序地找到、组装并运行你的代码。

整个过程可以分为三个核心阶段:构建 (Construction)实例化 (Instantiation)求值 (Evaluation)

1. 构建阶段 (Construction):绘制蓝图

这是整个流程的第一步,也是最基础的一步。在这个阶段,主要是为了找到所有的模块,并建立它们之间的依赖关系图

  • 入口文件识别:一切都从一个入口文件开始。在浏览器中,这通常是通过在 HTML 中使用 <script type="module" src="..."></script> 来指定的。
  • 依赖解析与下载
    • 浏览器或 Node.js 会解析入口文件,寻找 import 语句。
    • 每当遇到一个 import,它就会像一个寻宝游戏一样,根据你提供的路径(相对路径或绝对路径)去寻找并下载对应的模块文件。
    • 这个过程是递归的,如果下载的模块又 import 了其他模块,这个过程会一直持续下去,直到所有依赖的模块都被找到并下载。
  • 模块记录 (Module Record)
    • 下载下来的模块文件并不能直接被 JavaScript 引擎理解。 引擎需要将这些文件的代码解析成一种它能理解的数据结构,这就是模块记录 (Module Record)
    • 你可以把模块记录想象成每个乐高零件的详细信息卡片,上面记录着:
      • 这个模块依赖了哪些其他模块 (imports)。
      • 它自己又向外暴露了哪些变量或函数 (exports)。
      • 模块自身的代码(通常是抽象语法树 AST 的形式)。
  • 模块映射 (Module Map):为了避免重复下载和解析同一个模块,浏览器会维护一个模块映射 (Module Map)。 这个映射表的键 (key) 是模块的绝对路径,值 (value) 则是对应的模块记录。 如果遇到已经存在于映射表中的模块,就会直接复用,而不会重新下载。

构建阶段完成后,我们就得到了一张完整的依赖关系图,清楚地展示了所有模块以及它们之间的进出口关系。 但此时,代码还没有被执行,内存也还没有为模块中的变量分配空间。

2. 实例化阶段 (Instantiation):搭建骨架

有了蓝图之后,接下来就要开始搭建模型的骨架了。实例化阶段的核心任务是在内存中为所有模块的导出 (exports) 和导入 (imports) 建立连接

  • 内存空间分配:JavaScript 引擎会遍历所有的模块记录,并在内存中为所有 export 的变量和函数开辟空间。 但请注意,此时这些内存空间里还没有真正的值,只是一个占位符。
  • 链接导入与导出:这是最关键的一步,也正是 ES Modules 的精妙之处。引擎会将每个模块的 import 和它所依赖模块的 export 直接链接到同一个内存地址上
    • 这种连接方式被称为动态绑定 (Live Bindings)。 这意味着,当导出模块中的值发生变化时,所有导入该值的模块都能立即感知到这个变化,因为它们指向的是同一块内存。
    • 这与 CommonJS 的 require 机制有本质区别。CommonJS 导出的是值的拷贝,一旦导出,模块内部的变化不会影响到已经导入的值。
  • 模块环境记录 (Module Environment Record):为了管理模块内的变量,引擎会创建一个模块环境记录 (Module Environment Record)。 这个记录会跟踪模块内部的变量以及它们与内存地址之间的关系。

实例化阶段结束后,所有模块的导入和导出都建立起了实时的连接,形成了一个完整的模块实例图。 整个应用的骨架已经搭建完毕,就等着往里面填充血肉了。

3. 求值阶段 (Evaluation):注入灵魂

这是最后一步,也是真正让代码“活”起来的阶段。

  • 执行顶层代码:JavaScript 引擎会按照依赖关系的顺序,执行每个模块的顶层代码(即不在任何函数内部的代码)。
  • 填充值:在执行代码的过程中,之前在实例化阶段分配的内存空间会被真正地填充上值。 比如,当执行到 export const name = 'Gemini'; 这行代码时,name 变量所对应的内存地址就会被填入字符串 'Gemini'
  • 副作用:模块的代码只会被执行一次。 即使有多个模块导入了同一个模块,该模块的代码也只会在求值阶段被执行一遍。这可以防止意外的副作用,并确保模块状态的唯一性。

求值阶段完成后,所有模块的代码都已执行完毕,内存中的变量也都拥有了正确的值,你的应用程序就正式准备好运行了。

总结与对比

为了让你更好地理解,我们可以将 ES Modules 和更早的 CommonJS 规范进行一个简单的对比:

特性ES Modules (ESM)CommonJS (CJS)
加载方式异步加载,非阻塞。同步加载,会阻塞后续代码执行。
导入/导出值的引用 (动态绑定)。值的拷贝。
执行时机编译时确定依赖关系,运行时求值。运行时加载并执行。
语法静态的,import/export 必须在顶层。动态的,require 可以在代码块中。
this 指向模块顶层的 thisundefined指向当前模块。

希望这个从“绘制蓝图”到“注入灵魂”的比喻,能帮助你清晰地理解 ES Modules 的整个工作流程。它通过一种静态、高效的方式,为现代 JavaScript 应用提供了强大而可靠的模块化解决方案。