ES2015 module system

理解整个ES2015的模块系统和加载机制。

基础语法

语法部分,阮一峰的书讲的很清楚,然而语法并不是重要的地方:

  • import
  • export

article

深入浅出:

标准规范: 这类文章好硬,真难啃。

加载过程与机制

机制

ES6模块加载的机制,与CommonJS模块完全不同。CommonJS模块输出的是一个值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值。而ES6模块输出的是值的引,用此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。。
由于ES6输入的模块变量,只是一个”符号连接“,所以这个变量是只读的,对它进行重新赋值会报错。

过程

ES6将模块加载过程的细节完全交由最终的实现来定义,模块执行的其它部分倒是在规范中有详细定义。

粗略地讲,当你通知JS引擎运行一个模块时,它一定会按照以下四个步骤执行下去:

  1. 语法解析:阅读模块源代码,检查语法错误。
  2. 加载:递归地加载所有被导入的模块。这也正是没被标准化的部分。
  3. 连接:每遇到一个新加载的模块,为其创建作用域并将模块内声明的所有绑定填充到该作用域中,其中包括由其它模块导入的内容。
    如果你的代码中有import {cake} from "paleo"这样的语句,而此时“paleo”模块并没有导出任何“cake”,你就会触发一个错误。这实在是太糟糕了,你都快要运行模块中的代码了,都是cake惹的祸!
  4. 运行时:最终,在每一个新加载的模块体内执行所有语句。此时,导入的过程就已经结束了,所以当执行到达有一行import声明的代码的时候……什么都没发生!

静态模块系统:

  • 你只可以在模块的最外层作用域使用import和export,不可在条件语句中使用,也不能在函数作用域中使用import。
  • 所有导出的标识符一定要在源代码中明确地导出它们的名称,你不能通过编写代码遍历一个数组然后用数据驱动的方式导出一堆名称。
  • 模块对象被冻结了,所以你无法hack模块对象并为其添加polyfill风格的新特性。
  • 一个模块的所有依赖必须在模块代码运行前完全加载、解析并且及早连接,不存在一种通过import来按需懒加载的语法。
  • import模块产生的错误没有错误恢复机制。一个app可能囊括了上百个模块,一旦有一个模块无法加载或连接,所有的模块都不会运行,而且你不能在try/catch代码块中捕捉import的错误信息。(上面这些描述的本意是说:系统是静态的,webpack可在编译时为你检测那些错误。)
  • 不支持在模块加载依赖前运行其它代码的钩子,这也意味着无法控制模块的依赖加载过程。

目标

  • 优先默认导出 - 主要是为了简洁
  • 静态模型结构 - 编译时优化,支持未来新特性(宏定义、其它语言)
  • 支持循环依赖 - 解决大中型项目的一块通病
  • 同、异步加载 - 灵活加载模块,贴近当前的加载方案

静态模型结构

当前的模型系统都是在运行期才能知道导入、导出的是什么,这不利于性能优化,这是ES6在语言层面支持模块系统的重要原因。 从语言层面支持就可以通过强制的语法规则实现模型结构的静态化,这使得条件导入导出不可行,略微损失了一点灵活性,但因此带来的好处大大的。

  • 更快的查找 : 导入导出的都是静态代码,使得在编译时就能确定相互之前的依赖关系以及输入输出变量,模块的引用跟引用变量一样高效。而通过第三方库加载的模块是一个对象,引用时必须查找对象的属性,而属性是动态的,查找通常比较慢的。
  • 变量检测 : 在静态化的模型中,全局变量将逐渐消失,导入的模块内容都是可见的,模块内部的变量也都可以在编译前进行检测。这使得jslint、jshint所做的工作通过JS引擎就可以原生地支持了。
  • 宏定义 : 宏定义在ES6中并未涉及,但是很可能是未来的一个特性。目前的模块加载方案没法支持宏定义这样的需求,因为加载的模块只有在运行时才能知道其内容,而宏定义需要在编译时(运行前)进行替换,静态模型为可能出现的宏定义提供了向后兼容。
  • 类型检测 : 之所以需要类型,主要是为了性能优化。类型检测与宏定义类似,都要求编译时知道具体内容,没有静态模型的支持是完不成的。
  • 其他语言支持 : 如果想通过其他的编译语言为JS语言实现宏定义、静态类型的支持,这个时如果没有静态模型的支持,就没法在运行前将相关的代码编译成可执行的JS代码。