ES模块为JavaScript带来了官方的、标准化的模块系统。然而,它花了一段时间才走到这一步——将近10年的标准化工作。
但等待即将结束。随着火狐60在5月的发布(目前处于beta),所有主流浏览器都将支持ES模块,Node模块工作组目前正在努力将ES模块支持添加到Node.js中。WebAssembly的ES模块集成也在进行中。
许多JavaScript开发人员都知道ES模块一直备受争议。但很少有人真正了解ES模块是如何工作的。
让我们来看看ES模块解决了什么问题,以及它们与其他模块系统中的模块有什么不同。
What problem do modules solve?
仔细想想,用JavaScript编码就是管理变量。它就是给变量赋值,或者给变量加数字,或者把两个变量组合在一起,然后把它们放到另一个变量中。
因为你的很多代码都是关于改变变量的,你如何组织这些变量将会对你的编码有很大的影响,以及你如何维护这些代码。
一次只考虑几个变量会使事情更容易。JavaScript有一种方法可以帮助您做到这一点,称为作用域。由于JavaScript中作用域的工作方式,函数不能访问在其他函数中定义的变量。
这是很好的。这意味着当你研究一个函数时,你可以只考虑那个函数。你不必担心其他函数会对你的变量做什么。
不过,它也有一个缺点。这使得在不同函数之间共享变量变得困难。
如果你想在作用域之外共享你的变量该怎么办?处理这个问题的常用方法是将它放在你上面的作用域中,例如全局作用域中。
您可能还记得jQuery时代的这一点。在加载任何jQuery插件之前,必须确保jQuery处于全局作用域中。
这是可行的,但是会产生一些烦人的问题。
首先,所有的脚本标记都需要按正确的顺序排列。然后你必须小心确保没有人打乱这个顺序。
如果你打乱了顺序,那么在运行过程中,你的应用程序会抛出一个错误。当函数在它期望的地方(在全局变量上)寻找jQuery而没有找到时,它将抛出一个错误并停止执行。
这使得维护代码非常棘手。它使删除旧代码或脚本标记变成了一场轮盘赌游戏。你不知道什么东西会坏掉。代码的这些不同部分之间的依赖关系是隐式的。任何函数都可以获取全局函数上的任何东西,因此您不知道哪些函数依赖于哪些脚本。
第二个问题是,因为这些变量位于全局作用域中,所以在全局作用域中的代码的每一部分都可以更改变量。恶意代码可以故意更改该变量,使您的代码做一些您不希望它做的事情,或者非恶意代码可以偶然地破坏您的变量。
How do modules help?
当您使用模块进行开发时,您将建立一个依赖关系图。不同依赖项之间的连接来自您使用的任何导入语句。
通过这些导入语句,浏览器或Node可以准确地知道需要加载哪些代码。您给它一个文件作为图形的入口点。从这里开始,它只跟随任何导入语句来查找其余的代码。
但是文件本身并不是浏览器可以使用的东西。它需要解析所有这些文件,将它们转换为称为模块记录的数据结构。这样,它就知道文件中发生了什么。
在此之后,需要将模块记录转换为模块实例。实例结合了两件事:代码和状态。
代码基本上是一组指令。这就像是制作某样东西的食谱。但就其本身而言,您不能使用代码来做任何事情。你需要原材料来配合这些说明使用。
状态是什么?状态给你这些原材料。状态是变量在任何时间点的实际值。当然,这些变量只是内存中保存值的方框的别名。
因此,模块实例将代码(指令列表)与状态(所有变量的值)结合起来。
我们需要的是每个模块的一个模块实例。模块加载的过程是从这个入口点文件到拥有一个完整的模块实例图。
对于ES模块,这需要三个步骤。
- Construction — 查找、下载并将所有文件解析为模块记录。
- Instantiation —在内存中找到放置所有导出值的方框(但不要用值填充它们)。然后让导出和导入都指向内存中的这些框。这就是所谓的链接。
- Evaluation —运行代码,用变量的实际值填充方框。
人们说ES模块是异步的。您可以认为它是异步的,因为工作被分为这三个不同的阶段——加载、实例化和评估——并且这些阶段可以分别完成。
这意味着规范引入了一种CommonJS中没有的异步。我将在后面进一步解释,但是在CJS中,一个模块及其下面的依赖项都是一次性加载、实例化和求值的,中间没有任何中断。
然而,这些步骤本身并不一定是异步的。它们可以以同步的方式完成。这取决于是什么在加载。这是因为并非所有的事情都由ES模块规范控制。实际上工作分为两部分,由不同的规范覆盖。
The ES module spec规定了如何将文件解析为模块记录,以及如何实例化和计算该模块。然而,它并没有说明如何首先获得这些文件。
它是获取文件的加载器。装载机在不同的规格中有规定。对于浏览器来说,这个规范就是HTML规范。但是根据所使用的平台,可以使用不同的加载器。
加载器还精确地控制模块的加载方式。它调用ES模块的方法——ParseModule
, Module.Instantiate
, and Module.Evaluate
。它有点像控制JS引擎字符串的木偶师。
现在让我们更详细地了解每一步。