本文参考 https://hacks.mozilla.org/201...,建议大家读原文。
ES6发布了官方的,标准化的Module特性,这一特性花了整整10年的时间。但是,在这之前,大家也都在模块化地编写JS代码。比如在server端的NodeJS,它是对CommonJS的一个实现;Require.js则是可以在浏览器使用,它是对AMD的一个实现。
ES6官方化了模块,使得在浏览器端不再需要引入额外的库来实现模块化的编程(当然浏览器的支持与否,这里暂不讨论)。ES Module的使用也很简单,相关语法也很少,核心是import和export。但是,对于ES module到底是如何工作的,它又和之前的CommonJS和AMD有什么差别呢?这是接下来将要讨论的内容。
一:没有模块化的编程存在什么问题?
编写JS代码,主要是对于对变量的操作:给变量赋值或者变量之间进行各种运算。正因为大部分代码都是对变量的操作,所以如何组织代码里面的变量对于如何写好代码和代码维护就显得至关重要了。
当只有少量的变量需要考虑的时候,JavaScript提供了“scope(作用域)”来帮助你。因为在JavaScript里面,一个function不能访问定义在别的function里面的变量。
但是,这同时也带来一个问题,假如functionA想要使用functionB的变量怎么办呢?一个通用的办法就是把functionB的变量放到functionA的上一层作用域。典型的就是jQuery时代,如果要使用jQuery的API,先要保证jQuery在全局作用域。
但是这样做的问题也很多:
1: 所有的script标签必须保证正确的顺序,这使得代码的维护变得异常艰难。
2: 全局作用域被污染。
二:模块化编程如何解决上面提到的问题?
模块,把相关的变量和function组织到一起,形成一个所谓的module scope(模块作用域)。在这个作用域里面的变量和function之间彼此是可见的。
与function不同的是,一个模块可以决定自己内部的哪些变量,类,或者function可以被其他模块可见,这个决定我们叫做“export(导出)”。而其他的模块也就可以选择性地使用这个模块导出的内容,我们通过“import(导入)”来实现。
一旦有了导入和导出,我们就可以把我们的程序按照指责划分为一个个模块,大的模块可以继续划分为更小的模块,最终这些模块组合到一起,搭建起了我们整个程序,就像乐高一样。
三:ES Module的工作原理之Module Instances
当你在模块化编程的时候,你就会创建一棵依赖树。不同依赖之间的链接来源于你使用的每一条"import"语句。
就是通过这些"import"语句,浏览器和Node才知道它们到底要加载哪些代码。你给浏览器或者Node一个依赖树的入口文件,从这个入口文件开始,浏览器或者Node就沿着每一条"import"语句找到下面的代码。
但是,浏览器却使用不了这些文件。所有的文件都必须要转变为一系列被叫做“Module Records(模块记录)的数据结构,这样浏览器才能明白这些文件的内容。
在这之后,module record需要被转化为“module instance(模快实例)”。一个module instance包含2种东西:code和state。
code就是一系列的操作指令,就像菜单一样。但是,光有菜单,并不能作出菜,你还需要原材料。而state就是原材料。State就是变量在每一个特地时间点的值。当然,这些变量只是内存里面一个个保存着值的小盒子的小名而已。
而我们真正需要的就是每一个模块都有一个module instance。模块的加载就是从这个入口文件开始,最后得到包含所有module instance的完整图像。
四:Module Instances的产生步骤
对于,ES Module来说,这需要经历三个步骤:
1: Construction(构造)- 找到,下载所有的文件并且解析为module records。
2: Instantiation(实例化)- 在内存里找到所有的“盒子”,把所有导出的变量放进去(但是暂时还不求值)。然后,让导出和导入都指向内存里面的这些盒子。这叫做“linking(链接)”。
3: Evaluation(求值)- 执行代码,得到变量的值然后放到这些内存的“盒子”里。
大家都说ES Module是异步的。你可以认为它是异步的,因为这些工作被分成了三个不同的步骤 - loading(下载),instantiating(实例化)和evaluating(求值) - 并且这些步骤可以单独完成。
这意味着ES Module规范采用了一种在CommonJS里面不存在的异步机制。在CommonJS里面,对于一个模块和它底下的依赖来说,下载,实例化,和求值都是一次性完成的,步骤相互之间没有任何停顿。
然而,这并不意味这这些步骤必须是异步的,它们也可以同步完成。这依赖于“loading(下载)”是由谁去做的。因为,并不是所有的东西都由ES module规范控制。事实上,确实有两部分的工作是由别的规范负责的。
ES module规范 陈述了你应该怎样把文件解析为module records,和怎样初始化模块以及求值。然而,它却没有说在最开始要怎样得到这些文件。
是loader(下载器)去获取到了文件。而loader对于不同的规范来说是特定的。对于浏览器来说,这个规范是HTML 规范。你可以根据你所使用的平台来得到不同的loader。
loader也控制着模块如何加载。它会调用ES module的方法--ParseModule, Module.Instantiate,和Module.Evaluate。loader就像傀儡师,操纵着JS引擎的线。
现在让我们来具体聊一聊每一个步骤。
五:Module Instances的产生步骤之Construction
对于每一个模块来说,在这一步会经历以下几个步骤
1: 弄清楚去哪里下载包含模块的文件(又叫“ module resolution(模块识别)”)
2: 获取文件(通过从一个URL下载或者从文件系统加载)
3: 把文件解析为module record(模块记录)
step1: Finding the file and fetching it 找到文件并获取文件
loader会负责找到文件并下载。首先,需要找到入口文件,在HTML文件里,我们通过使用