深刻理解es module

模块系统的做用

传统script标签的代码加载容易致使全局做用域污染,并且要维系一系列script的书写顺序,项目一大,维护起来愈来愈困难。模块系统经过声明式的暴露和引用模块使得各个模块之间的依赖变得明显。javascript

es module如何工做的

这部分推荐去看es-modules-a-cartoon-deep-dive,原文里有图,如下的内容是我的理解整理。html

分三步:java

  1. 构造,寻找而且下载全部的文件而且解析成模块记录(Module Records)(包含当前模块代码的抽象语法树,当前模块的依赖模块的信息)。
  2. 实例化,将模块记录实例化将各个模块之间的import,export部分对应的都在内存中指向到一块儿(linking)
  3. 执行,将import, export内存里指向的地址填上实际的值。

构造阶段(Construction)

构造阶段要作三件事情:node

解释(interpret)import后的模块指示符(module specifier)成实际url或者文件地址

不一样平台根据本身平台的模块解析算法(Module Resolution Algorithm)解释模块指示符,浏览器端目前只接受url作为指示符。不过浏览器未来会一样支持内置模块好比kv-storagegit

模块指示符里的变量

模块指示符里不能有变量可是node中commonJS是能够有的,由于在commonJS的模块代码里,require声明前的代码是会先执行的,es module是最后一步再去执行,这一步才知道各个变量的具体值是多少。因此能够在node中有以下写法:es6

require(`${path}/sum.js`);
复制代码

不过es module里有另外一种写法动态引入import()能够支持在代码执行时动态引入模块,能够在指示符里携带变量github

import(`${path}/sum.js`);
复制代码

浏览器根据url下载文件或者node根据文件地址去加载文件

将文件解析成模块记录

浏览器解析常规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文件里导出的objmain.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)

原文中是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里的aundefined。由于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中的值也在以后能取到值了。

es module的好处

  1. 动态绑定解决了循环调用的问题(见上文)
  2. 静态分析(statically analysis) 由于在代码未执行阶段就已经知道当前模块导入了什么,导出了什么,因此有些工具就能够进行静态分析。好比vscode中引入模块代码时会提示当前模块里导出的内容。

es module的坏处

  1. 兼容性
  2. 还没有有针对node的解决方案

参考资料

es-modules-a-cartoon-deep-dive

module-exports

ecma262

相关文章
相关标签/搜索