传统script标签的代码加载容易致使全局做用域污染,并且要维系一系列script的书写顺序,项目一大,维护起来愈来愈困难。模块系统经过声明式的暴露和引用模块使得各个模块之间的依赖变得明显。javascript
这部分推荐去看es-modules-a-cartoon-deep-dive,原文里有图,如下的内容是我的理解整理。html
分三步:java
构造阶段要作三件事情:node
不一样平台根据本身平台的模块解析算法(Module Resolution Algorithm)解释模块指示符,浏览器端目前只接受url作为指示符。不过浏览器未来会一样支持内置模块好比kv-storage。git
模块指示符里不能有变量可是node中commonJS是能够有的,由于在commonJS的模块代码里,require
声明前的代码是会先执行的,es module是最后一步再去执行,这一步才知道各个变量的具体值是多少。因此能够在node中有以下写法:es6
require(`${path}/sum.js`);
复制代码
不过es module里有另外一种写法动态引入import()
能够支持在代码执行时动态引入模块,能够在指示符里携带变量github
import(`${path}/sum.js`);
复制代码
浏览器解析常规js文件时会解析完后再执行。和模块的解析策略不同,这里要告诉浏览器解析的是个模块。在html中:算法
<script type="module"> import {sum} from "./sum.js" </script>
复制代码
ps: 在node中由于没有浏览器这种相似打tag的形式,有种方案是模块文件是.mjs
后缀结尾的方案,不过目前还没有敲定。浏览器
解析模块文件为模块记录,找到依赖的模块再去下载模块而后解析成模块记录,直到全部的模块都解析成模块记录为止。模块记录会存在当前全局的一个模块映射里(Module Map),能够理解成一个缓存,下次再有相同url的模块请求就直接从模块映射里拿出模块记录便可。缓存
将上面获得的模块记录类实例化。 首先在内存中指定位置给各个模块的export
导出的变量或者函数,接着将模块中对应的import
部分一样指向对应的export
的内存地址。 举个🌰
// main.js
import {obj} from "./obj.js"
// obj.js
const obj = {a: 123};
export {obj}
复制代码
obj.js
文件里导出的obj
和main.js
文件里引用的obj
是指向同一个内存地址的,这中方法就是动态绑定(live binding)。
<script type='module'> import {obj} from "./obj.js" console.log(obj); //{a: 123} setTimeout(() => { console.log(obj) //{b: 233} }, 2000); </script>
复制代码
let obj = {
a: 123
};
setTimeout(() => {
obj = { b: 233 };
}, 1000);
export { obj };
复制代码
下面咱们看下node中一样的代码的效果。
// test1.js
var obj = require("./test2.js");
console.dir(obj); // {a: 123}
setTimeout(() => {
console.dir(obj); // {a: 123}
}, 2000);
// test2.js
let obj = { a: 123 };
setTimeout(() => {
obj = { b: 233 };
}, 1000);
module.exports = obj;
复制代码
在commonJS中require
一个对象是在内存中复制一份导出模块的对象。动态绑定主要解决的问题就是循环引用的问题,循环引用在下面的执行阶段进行解释。 注意: es module中能够在模块导出的部分更改导出值如上面代码所示,可是不能在引入部分更改。
import {obj} from "./sum.js"
obj = '233' // Uncaught TypeError: Assignment to constant variable.
复制代码
如上报错会提示不能给常量赋值,不过若是是对象的话能够更改内部的key,因为动态绑定的缘由,导出部分也会发生改变
// main.js
import {obj} from "./obj.js"
setTimeout(() => {
obj.a = '嘻嘻'
}, 1000);
// obj.js
let obj = { a: 123 };
console.log(obj); // {a: 123}
setTimeout(() => {
console.log(obj); // {a: "嘻嘻"}
}, 2000);
export { obj };
复制代码
原文中是evaluate,我这里理解成了执行,若有不对欢迎指出。引擎开始执行模块了,每一个模块只会被执行一次。在上面提到过的module map里的模块记录里会存有当前模块的状态是实例化中仍是实例完成仍是执行完成等。能够避免同一个模块文件被屡次执行。
以下在node中,两个模块互相引用。
// test1.js
var b = require("./test2").b;
console.dir("test1: " + b); // 'test1: test2' 🥈
var a = "test1";
exports.a = a;
// test2.js
var a = require("./test1").a;
console.log("test2: " + a); // test2: undefined 🥇
var b = "test2";
setTimeout(() => {
console.log("test2: " + a); // test2: undefined 🥉
}, 1000);
exports.b = b;
node test1.js // 启动
复制代码
ps: emoji里表示打印顺序 node执行某个模块时会将当前模块的代码放入函数中,向这个函数传递module
, module.exports
, __dirname
等参数。初始的module
就是一个空对象。 test1.js执行遇到require('./test2)
时会进入test2模块开始执行,这个时候又碰到引用test1模块的东西;由于test1模块没有执行完成,它的module.exports
仍是空对象,因此这个时候test2里的a
是undefined
。由于commonJS不是动态绑定的,so等到test1模块执行完a
变量里仍是undefined
es module
// es1
import { b } from "./es2.js";
console.log("es1: " + b); // es1: es2 🥈
var a = "es1";
export { a };
// es2
import { a } from "./es1.js";
console.log("es2: " + a); // es2: undefined 🥇
var b = "es2";
setTimeout(() => {
console.log("es2: " + a); // es2: es1 🥉
}, 1000);
export { b };
复制代码
以上代码入口是es1文件。根据打印顺序来看先是执行的es2模块,以后es1里的a
填充了实际值,因为是动态绑定es2中的a
中的值也在以后能取到值了。