深入ES6模块(Module)

112 阅读5分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

本文没有介绍EMS的用法,只在理论和原理层面讨论。

目录

  1. 模块是什么?
  2. 模块的核心功能
  3. ES 模块如何工作
  4. 构建工具
  5. 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 模块来说,这主要有三个步骤:

  1. 构造 —— 查找、下载并解析所有文件到模块记录中。
  2. 实例化 —— 在内存中寻找一块区域来存储所有导出的变量(但还没有填充值)。然后让 export 和 import 都指向这些内存块。这个过程叫做链接(linking)。
  3. 求值 —— 运行代码,在内存块中填入变量的实际值。

人们说 ES 模块是异步的。你可以把它当作时异步的,因为整个过程被分为了三阶段 —— 加载、实例化和求值 —— 这三个阶段可以分开完成。

这意味着 ES 规范确实引入了一种在 CommonJS 中并不存在的异步性。我稍后会再解释,但是在 CJS 中,一个模块和其下的所有依赖会一次性完成加载、实例化和求值,中间没有任何中断。

当然,这些步骤本身并不必须是异步的。它们可以以同步的方式完成。这取决于谁在做加载这个过程。这是因为 ES 模块规范并没有控制所有的事情。实际上有两部分工作,这些工作分别由不同的规范控制。

ES模块规范说明了如何将文件解析到模块记录,以及如何实例化和求值该模块。但是,它并没有说明如何获取文件。

是加载器来获取文件。加载器在另一个不同的规范中定义。对于浏览器来说,这个规范是 HTML 规范。但是你可以根据所使用的平台有不同的加载器。

加载器还精确控制模块的加载方式。它调用 ES 模块的方法 —— ParseModuleModule.Instantiate 和 Module.Evaluate。这有点像通过提线来控制 JS 引擎这个木偶。

构建工具

在实际使用的过程中,我们应该不会直接接触原始的模块导入,更多的是使用构建工具,如webpack。然后部署到服务器

构建工具做了以下的工作:

  1. 从一个打算放在 HTML 中的 <script type="module"> “主”模块开始。

  2. 分析它的依赖:它的导入,以及它的导入的导入等。

  3. 使用所有模块构建一个文件(或者多个文件,这是可调的),并用打包函数(bundler function)替代原生的 import 调用,以使其正常工作。还支持像 HTML/CSS 模块等“特殊”的模块类型。

  4. 在处理过程中,可能会应用其他转换和优化:

    • 删除无法访问的代码。

    • 删除未使用的导出(“tree-shaking”)。

    • 删除特定于开发的像 console 和 debugger 这样的语句。

    • 可以使用 Babel 将前沿的现代的 JavaScript 语法转换为具有类似功能的旧的 JavaScript 语法。

    • 压缩生成的文件(删除空格,用短的名字替换变量等)。

ESM和commonJS对比

ESMcommonJS
异步同步
模块输出的是值的引用模块输出的是值的拷贝
编译时加载(也可以动态加载)运行时加载