Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
本文没有介绍EMS的用法,只在理论和原理层面讨论。
目录
- 模块是什么?
- 模块的核心功能
- ES 模块如何工作
- 构建工具
- ESM与CommonJs对比
模块是什么?
模块就是一个文件,一个脚本就是一个模块。
模块可以互相加载,并可以使用特殊的指令 export 和 import 来交换功能,从另一个模块调用一个模块的函数:
export关键字标记了可以从当前模块外部访问的变量和函数。import关键字允许从其他模块导入功能。
由于模块支持特殊的关键字和功能,因此我们必须通过使用 <script type="module"> 特性(attribute)来告诉浏览器,此脚本应该被当作模块(module)来对待。
模块的核心功能
-
始终使用“use strict”
-
每个模块都有自己的顶级作用域
-
模块只在第一次导入时被解析
如果同一个模块被导入到多个其他位置,那么它的代码只会执行一次,即在第一次被导入时。然后将其导出(export)的内容提供给进一步的导入(importer)。
-
import.meta 包含当前模块信息
-
在一个模块中,“this” 是 undefined
ESM是怎样工作的?
这里翻译自 Hacks blog post by Lin Clark: ES modules: A cartoon deep-dive
使用模块开发时,会建立一个依赖图。不同依赖项之间的连接来自你使用的各种 import 语句。
浏览器或者 Node 通过 import 语句来确定需要加载什么代码。你给它一个文件来作为依赖图的入口。之后它会随着 import 语句来找到所有剩余的代码。
但浏览器并不能直接使用文件本身。它需要把这些文件解析成一种叫做模块记录(Module Records)的数据结构。这样它就知道了文件中到底发生了什么。
之后,模块记录需要转化为模块实例(module instance)。一个实例包含两个部分:代码和状态。
代码基本上是一组指令。就像是一个告诉你如何制作某些东西的配方。但你仅依靠代码并不能做任何事情。你需要将原材料和这些指令组合起来使用。
什么是状态?状态就是给你这些原材料的东西。指令是所有变量在任何时间的实际值的集合。当然,这些变量只是内存中保存值的数据块的名称而已。
所以模块实例将代码(指令列表)和状态(所有变量的值)组合在一起。
我们需要的是每个模块的模块实例。模块加载就是从此入口文件开始,生成包含全部模块实例的依赖图的过程。
对于 ES 模块来说,这主要有三个步骤:
- 构造 —— 查找、下载并解析所有文件到模块记录中。
- 实例化 —— 在内存中寻找一块区域来存储所有导出的变量(但还没有填充值)。然后让 export 和 import 都指向这些内存块。这个过程叫做链接(linking)。
- 求值 —— 运行代码,在内存块中填入变量的实际值。
人们说 ES 模块是异步的。你可以把它当作时异步的,因为整个过程被分为了三阶段 —— 加载、实例化和求值 —— 这三个阶段可以分开完成。
这意味着 ES 规范确实引入了一种在 CommonJS 中并不存在的异步性。我稍后会再解释,但是在 CJS 中,一个模块和其下的所有依赖会一次性完成加载、实例化和求值,中间没有任何中断。
当然,这些步骤本身并不必须是异步的。它们可以以同步的方式完成。这取决于谁在做加载这个过程。这是因为 ES 模块规范并没有控制所有的事情。实际上有两部分工作,这些工作分别由不同的规范控制。
ES模块规范说明了如何将文件解析到模块记录,以及如何实例化和求值该模块。但是,它并没有说明如何获取文件。
是加载器来获取文件。加载器在另一个不同的规范中定义。对于浏览器来说,这个规范是 HTML 规范。但是你可以根据所使用的平台有不同的加载器。
加载器还精确控制模块的加载方式。它调用 ES 模块的方法 —— ParseModule、Module.Instantiate 和 Module.Evaluate。这有点像通过提线来控制 JS 引擎这个木偶。
构建工具
在实际使用的过程中,我们应该不会直接接触原始的模块导入,更多的是使用构建工具,如webpack。然后部署到服务器
构建工具做了以下的工作:
-
从一个打算放在 HTML 中的
<script type="module">“主”模块开始。 -
分析它的依赖:它的导入,以及它的导入的导入等。
-
使用所有模块构建一个文件(或者多个文件,这是可调的),并用打包函数(bundler function)替代原生的
import调用,以使其正常工作。还支持像 HTML/CSS 模块等“特殊”的模块类型。 -
在处理过程中,可能会应用其他转换和优化:
-
删除无法访问的代码。
-
删除未使用的导出(“tree-shaking”)。
-
删除特定于开发的像
console和debugger这样的语句。 -
可以使用 Babel 将前沿的现代的 JavaScript 语法转换为具有类似功能的旧的 JavaScript 语法。
-
压缩生成的文件(删除空格,用短的名字替换变量等)。
-
ESM和commonJS对比
| ESM | commonJS |
|---|---|
| 异步 | 同步 |
| 模块输出的是值的引用 | 模块输出的是值的拷贝 |
| 编译时加载(也可以动态加载) | 运行时加载 |