ES modules :A cartoon deep-dive
栏目: JavaScript · 发布时间: 5年前
内容简介:翻译自文章:尽管花了10年之久的时间,JavaScript还是有了官方,标准的模块系统:等待快要结束了,随着Firefox 60的发布,所有的主流浏览器都支持
翻译自文章: ES modules A cartoon deep-dive
尽管花了10年之久的时间,JavaScript还是有了官方,标准的模块系统: Es modules
。
等待快要结束了,随着Firefox 60的发布,所有的主流浏览器都支持 Es modules
了,而且Node的模块工作组也在开始在 Node.js
中支持 Es modules
的工作了(译注: 已经部分支持),而且 Es modeule对WebAssembly的支持 也已经在计划当中了。
许多JavaScript开发者都知道颇具争议的 Es modules
,但是很少人真正知道它是如何工作的。
下文让我们看看 Es modules
解决了什么问题,以及它和其他模块系统的不同之处。
模块解决了什么问题?
当我们说JavaScript编码时,讲的几乎是变量的管理。不外乎是变量的赋值,对变量增加数值,或者将两个变量加起来赋值给另外的变量。
大量的代码都是变量的改变,因此如何组织这些变量直接影响你编码的质量甚至也影响你能否更好的维护代码。
如果只是少量的变量,JavaScript中的 scope
就可以一劳永逸的解决好这个问题。这个得益于 scope
的工作方式,函数并不能访问其他函数中的变量。
这点很好,你在一个函数中编码时候,你不担心其他的函数会操作你的变量。
但它也有不好的一面,这样就很难在函数间共享变量。
如果你确实需要在 scope
之外共享变量呢? 通常的做法是将其放到你的上一层……,譬如全局的 global scope
中。
你可能还记得,在 jQuery
时代,在你加载jQuery 插件之前,你得确保 jQuery
是在 global scope
中。
这样也行,但还是会有些让你恼火的问题。
首先,你所有的script标签都得保证正确的顺序,得保证任何一个不能把顺序弄错了。
一旦你弄错了顺序,你的app就会在运行时候报错。当函数在 global scope
中拿不到期望的 jQuery
变量时候就会报错,然后停止运行。
这样就会让代码维护变得很困难。移除旧代码就像玩轮盘游戏一样,你永远不知道什么时候会出错。不同模块间的依赖也模糊不清。任何函数都可以操作 global scope
,你也就不知道哪个函数依赖哪个脚本。
另一个问题是, global scope
之内的任何代码都能改变其中的变量,恶意代码会让不怀好意的更改某些变量,或者甚至正常代码也可能会意外改变了某些变量。
modules将会如何解决这些问题?
模块是一个种更好管理这些变量和函数的方法。你可以将相关的变量和函数使用模块来组织到一起。
这样就将变量和函数放到了模块 scope
中。其中的函数就可以在模块 scope
共享变量。
和函数 scope
不一样,模块将其中的变量分享给其他模块,而且模块可以决定分享哪些变量,类,或者函数。
这种分享叫做导出 export
。一旦导出了,其他模块就可以生命自己依赖这个模块中的哪个变量,类或者函数。
如此精确的关系,当你删除了一方,自然很容易知道哪些地方会出错。
当你可以在模块间导入和导出变量的时候,你就可以更容易地将代码分成彼此独立的小块。然后你就可以像乐高一样,同样的模块集组合出各种不同的应用。
模块很有用,也有多次给JavaScript添加模块的尝试。现在还在使用还有两个。Node.js一直以来使用的 CommonJS(CJS)
,以及最近才被添加到JavaScript标准中的 ESM(EcmaScript modules)
,浏览器已经支持 ESM
了,Node.js也在支持之中。
来让我们深入了解es模块的工作方式。
ES模块工作方式
当你使用模块开发的时候,你会建立一个依赖图,其中不同依赖之间的连接就来自于你所使用的导入声明。
这些导入声明就是让浏览器或者Node知道需要加载哪些代码。你给定一个文件作为依赖图的入口,浏览器就会按照导入声明来依次加载代码。
但是浏览器不会使用文件本身。它需要将所有的文件解析为一种叫做模块记录 Module Records
的数据结构才能让浏览器了解文件内容。
然后,模块记录需要转变为模块实例。一个实例结合了两个东西:规则 code
和状态 state
。
规则 code
就是指令的集合。它就像是菜谱一样。但是对它本身来说,你需要新鲜的材料才能有用。
那么什么是状态 state
呢?状态 state
就提供的新鲜材料。状态 state
就是变量在任意时刻的值。当然,变量实际上是内存中持有值的盒子的昵称。
因此模块实例就是规则(指令集合)和状态(所有的变量的值)的结合。
我们所需要的是每个模块的模块实例。那么模块加载就是从入口到得到全图模块实例的过程。
对于 Es modules
模块来说,会有三个步骤。
- 构建 : 查找,下载,并且解析所有文件到模块记录
- 实例化 : 找到内存中能够放置所有导出值的盒子(暂时先不要填充值)。然后将导出和导入指向内存中的这些盒子。这步叫做连接。
- 执行 : 执行代码然后将变量的真实值填入盒子。
一般说 Es modules
是异步的。你可以将这个异步理解为它的工作非常了这三个步骤——加载,实例化,执行,而且这些阶段是可以单独分开完成。
这也意味着,上述的这种异步特性是 CommonJS(CJS)
所没有的(下文解释),在一个CJS模块中,加载,实例化,执行是一次完成的,中间没有停顿。
然而,这些步骤并不是一定需要异步。他们也可以同步地来完成。这取决于谁在做加载。这是因为ES module 标准并没有控制所有的事情,工作的两部分由两个不同的规范所覆盖。
Es module 规范 规定了如何解析文件为模块记录 Module Records
,以及如何实例化和执行模块。但是,最开始的如何拿到文件并没有涉及。
文件是由加载器拉去文件,但是加载器又有着不同的规范。对浏览器来就是 HTML规范 。而且平台不同你也可以有不同的加载器。
加载器也是实际控制了模块是如何加载的。它执行ES模块的方法—— ParseModule
, Module.Instantiate
, and Module.Evaluate
,它就像线绳一样控制这JS引擎这个木偶。
现在让我们更详尽地每个步骤。
构建 Construction
每个模块在构建阶段会发生三件事情:
- 弄清楚哪里去下载包含模块的文件(也就是模块分解)
- 拉取文件(通过URL下载或者从文件系统中加载)
- 解析文件为模块记录
查找和拉取文件
加载器负责超着和下载文件。首先它需要找到入口文件。在HTML中,开发者通过script 标签告诉加载器。
那它又怎么拿到下一批的模块呢—— main.js
中直接依赖的文件。
这就是导入声明的用处了。导入声明中有一部分那个叫做模块标识符 module specifier
。它告诉加载器哪里可以找到下一个模块。
模块标识符 module specifier
还有一点需要注意的是:还需要处理浏览器和Node两种不同的情况。它们使用各自的模块分析算法去解释模块标识符上的字符串,而且不同平台还不一样。目前一些能够在Node中工作的模块标识符在浏览器中并不能工作,不过目前 已在在着手修复这个问题了 。
在这之前,浏览器只接受URl作为模块的标识符。它将会通过URL来加载模块文件。而且也不会一次性加载全模块图中的文件。因为你不解析文件你就没办法知道文件中的依赖……你也不能解析文件除非你拉取到了文件。
这也就意味着我们需要一层一层的经过这个依赖树:解析一个文件,得到所依赖的文件,然后再查找和加载这些依赖。
如果主线程等待每个文件的下载,那么就会有很多其他的任务悬挂在队列里了。
因为我们在浏览器中,下载部分通常会花费很长的是将。
如此的阻塞主线程会让我们的模块无法使用。这也是为什么ES module 规范中将算法分为几个阶段的原因之一。把模块构建分出去平台自己实现,也就容许浏览器能够拉取文件然后在同步实例化之前建立自己对模块图的理解。
这个实现——将算法分成几个步骤,是ES 模块和 CommonJS
模块的主要区别。
CommonJS
能够不这么做的原因在于,从文件系统中加载文件花费的时间远远小于从网络下载。也就是Node可以在加载文件的时候阻塞主线程,而且一旦文件已经加载,直接实例化和执行也就很顺理成章(没有分成几个步骤),也就是在返回模块实例之前,已经实例化,执行全树的文件了。
CommonJS
的实现还有几点说明,我会在稍后说明。但是其中一点是Node的 CommonJS
模块标识符中是容许使用变量的。因为它在寻找下一个模块的时候已经执行了次模块的所有代码,已经在模块分析之前拿到了变量的值。
但是对于 Es modules
来说,在做任何代码执行之前你需要拿到全部的模块依赖图。也就是说在模块分析模块标识符的时候,变量还没有被赋值。
但是,对于某些情况模块路径中有变量是很用的。比如,你可能需要根据代码执行或者环境不同来加载不同的模块哦。
为了能够在 Es modules
中实现这一点,就有了 动态导入 dynamic import
的提议。这样就可以使用, import(
${path}/foo.js )
这样的表述。
这样能够工作的原因在于,将 import()
当成一个新的模块依赖图的入口。动态导入开启了一个新的模块依赖图,它也会被分开处理。
还有一点需要注意的是,不同模块图中共有的模块会共享同一个模块实例。这是因为加载器会缓存模块实例。对于同一个全局作用域下的每个模块只会有一个模块实例。
这也会减少引擎的工作。例如,被不同模块所依赖模块文件只会被拉取一次。这也是模块实例需要被缓存的原因,我们还会在执行阶段将这个问题。
加载器会使用叫 模块地图 module map
的东西来管理模块缓存。不同的全局环境使用各自的模块地图。
当加载器开始拉取一个URL时候,它会将这个URL放入地图并且标记为正在拉取文件。然后他会发起请求,进入下一个文件的拉取。
如果另外的模块依赖了同样的文件,加载器将会查看地图中的每个URL,如果它看到了 fetching
的存在,它会直接进入下一个URL。
模块地图不是仅仅缓存了被拉取的文件,他还是用于模块的缓存,我们接下来来看。
解析
当我们加载完文件后,我们需要将其解析为模块记录。这样才能帮助理解浏览器每个模块的不同。
一旦模块记录创建之后,它就会被放置在模块地图中,当它再次被需要的时候,加载器就直接从模块地图中直接取出。
解析还有一点值得注意的是,所有的模块会当作顶部有 use strict
来解析。还有点不同的,模块的顶层中 await
关键词不容许使用, this
的值为 undefined
。
这种不同的解析叫做“解析球门”,同一文件,你是用不同的球门来解析,你会得到不同的结果。因此你需要在解析之前知道你的球门是什么——是否是模块。
在浏览器中这很简单,你只需要在script标签中使用 type="module"
即可。
但是在Node中,你没有HTML标签能够使用,也就没有 type
属性。社区中一个方法是使用 .mjs
新的扩张 ,这些讨论在进行,社区也暂时未确定使用何种方式。
无论如何,加载器会决定是否按照模块来解析文件。如何它是一个模块而且还有依赖,它就会开始一遍遍的处理直到所有的文件被拉取和解析。
但我们做完加载的环节,你就会从一个入口文件得到一批的模块记录。
下一步就是实例化模块,然后将所有的实例连接起来。
未完……
以上所述就是小编给大家介绍的《ES modules :A cartoon deep-dive》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
网络经济的十种策略
(美)凯文・凯利 / 肖华敬/任平 / 广州出版社 / 2000-06 / 26.00元
全书介绍网络经济的十个新游戏规则,分别是:蜜蜂比狮子重要;级数比加法重要;普及比稀有重要;免费比利润重要;网络比公司重要;造山比登山重要;空间比场所重要;流动比平衡重要;关系比产能重要;机会比效率重要!一起来看看 《网络经济的十种策略》 这本书的介绍吧!