[前端漫谈_5] 从 IIFE 聊到 Babel 带你深刻了解前端模块化发展体系

前言

做为一名前端工程师,天天的清晨,你走进公司的大门,回味着前台妹子的笑容,摘下耳机,泡上一杯茶,打开 Terminal 进入对应的项目目录下,而后 npm run start / dev 或者 yarn start / dev 就开始了一天的工做。javascript

当你须要进行时间的转换只须要使用 dayjs 或者 momentjs, 当你须要封装 http 请求的时候,你能够用 fetch 或者 axios, 当你须要作数据处理的时候,你可能会用 lodash 或者 underscorehtml

不知道你有没有意识到,对于今天的咱们而言,这些工具包让开发效率获得了巨大的提高,可是这一切是从什么开始的呢?前端

这些就要从 Modular design (模块化设计) 提及:java

Modular design (模块化设计)

在我刚接触前端的时候,常常据说 Modular design (模块化设计) 这样的术语,面试时也会常常被问到,“聊聊前端的模块化”这样的问题,或许不少人均可以说出几个熟悉的名词,甚至是他们之间的区别:node

  • IIFE [Immediately Invoked Function Expression]
  • Common.js
  • AMD
  • CMD
  • ES6 Module

但就像你阅读一个项目的源码同样,若是从第一个 commit 开始研究,那么你能收获的或许不只仅是,知道他们有什么区别,更重要的是,可以知道在此以前的历史中,是什么样的缘由,致使了区别于旧的规范而产生的新规范,而且基于这些,或许你可以从中体会到这些改变意味着什么,甚至在未来的某个时刻,你也能成为这规则的制定者之一jquery

因此让咱们回到十年前,来看看是怎么实现模块化设计的:ios

IIFE

IIFE 是 Immediately Invoked Function Expression 的缩写,做为一个基础知识,不少人可能都已经知道 IIFE 是怎么回事,(若是你已经掌握了 IIFE,能够跳过这节阅读后面的内容) 但这里咱们仍旧会解释一下,它是怎么来的,由于在后面咱们还会再次提到它:git

最开始,咱们对于模块区分的概念,多是从文件的区分开始的,在一个简易的项目中,编程的习惯是经过一个 HTML 文件加上若干个 JavaScript 文件来区分不一样的模块,就像这样:es6

咱们能够经过这样一个简单的项目来讲明,来看看每一个文件里面的内容:github

demo.html

这个文件,只是简单的引入了其余的几个 JavaScript 文件:

<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>demo</title>
</head>
<script src="main.js"></script>
<script src="header.js"></script>
<script src="footer.js"></script>

<body></body>

</html>
复制代码

其余三个 JavaScript 文件

在不一样的 js 文件中咱们定义了不一样的变量,分别对应文件名:

var header = '这是一条顶部信息' //header.js
var main_message = '这是一条内容信息'   //main.js
var main_error = '这是一条错误信息'   //main.js
var footer = '这是一条底部信息' //footer.js
复制代码

像这样经过不一样的文件来声明变量的方式,实际上没法将这些变量区分开来。

它们都绑定在全局的 window / Global(node 环境下的全局变量) 对象上,尝试去打印验证一下:

这简直就是一场噩梦,你可能没有意识到这会致使什么严重的结果,咱们试着在 footer.js 中对 header 变量进行赋值操做,让咱们在末尾加上这样一行代码:

header = 'nothing'
复制代码

打印后你就会发现,window.header 的已经被更改了:

试想一下,你永远没法预料在何时什么地点无心中就改掉了以前定义的某个变量,若是这是在一个团队中,这是一件多么可怕的事情。

Okay,如今咱们知道,仅仅经过不一样的文件,咱们没法作到将这些变量分开,由于它们都被绑在了同一个 window 变量上。

可是更重要的是,怎么去解决呢?咱们都知道,在 JavaScript 中,函数拥有本身的做用域 的,也就是说,若是咱们能够用一个函数将这些变量包裹起来,那这些变量就不会直接被声明在全局变量 window 上了:

因此如今 main.js 的内容会被修改为这样:

function mainWarraper() {
  var main_message = '这是一条内容信息' //main.js
  var main_error = '这是一条错误信息' //main.js
  console.log('error:', main_error)
}

mainWarraper()
复制代码

为了确保咱们定义在函数 mainWarraper 的内容会被执行,因此咱们必须在这里执行 mainWarraper() 自己,如今咱们在 window 里面找不到 main_messagemain_error 了,由于它们被隐藏在了 mainWarraper 中,可是 mainWarraper 仍旧污染了咱们的 window:

这个方案还不够完美,怎么改进呢?

答案就是咱们要说的 IIFE 咱们能够定义一个 当即执行的匿名函数 来解决这个问题:

(function() {
  var main_message = '这是一条内容信息' //main.js
  var main_error = '这是一条错误信息' //main.js
  console.log('error:', main_error)
})()
复制代码

由于是一个匿名的函数,执行完后很快就会被释放,这种机制不会污染全局对象。

虽然看起来有些麻烦,但它确实解决了咱们将变量分离开来的需求,不是吗?然而在今天,几乎没有人会用这样方式来实现模块化编程。

后来又发生了什么呢?

CommonJS

在 2009 年的一个冬天, 一名来自 Mozilla 团队的的工程师 Kevin Dangoor 开始捣鼓了一个叫 ServerJS 的项目,他是这样描述的:

"What I’m describing here is not a technical problem. It’s a matter of people getting together and making a decision to step forward and start building up something bigger and cooler together."

"在这里我描述的不是一个技术问题。 这是一个关于你们齐心协力,作出决定向前迈进,而且开始一块儿建造一些更大更酷的东西的问题。"

这个项目在 2009 年的 8 月份改名为今日咱们熟悉的 CommonJS 以显示 API 更普遍的适用性。我以为那时他可能并无料到,这一规则的制定会让整个前端发生翻天覆地的变化。

CommonJS 在 Wikipedia 中是这样描述的:

CommonJS is a project with the goal to establish conventions on module ecosystem for JavaScript outside of the web browser. The primary reason of its creation was a major lack of commonly accepted form of JavaScript scripts module units which could be reusable in environments different from that provided by a conventional web browser e.g. web server or native desktop applications which run JavaScript scripts.

CommonJS 是一个旨在 Web 浏览器以外,为 JavaScript 创建模块生态系统的约定的项目。 其建立的主要缘由是缺少广泛接受的 JavaScript 脚本模块单元形式,而这一形式可让 JavaScript 在不一样于传统网络浏览器提供的环境中重复使用,例如, 运行 JavaScript 脚本的 Web 服务器或本机桌面应用程序。

经过上面这些描述,相信你已经知道 CommonJS 是诞生于怎样的背景,可是这里所说的 CommonJS 是一套通用的规范,与之对应的有很是多不一样的实现:

图片来源于 wiki

可是咱们关注的是其中 Node.js 的实现部分

Node.js Modules

这里不会解释 Node.js Modules 的 API 基本用法,由于这些均可以经过阅读 官方文档 来了解,咱们会讨论为何会这样设计,以及你们比较难理解的点来展开。

在 Node.js 模块系统中,每一个文件都被视为一个单独的模块,在一个Node.js 的模块中,本地的变量是私有的,而这个私有的实现,是经过把 Node.js 的模块包装在一个函数中,也就是 The module wrapper,咱们来看看,在 官方示例中 它长什么样:

(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
// 实际上,模块内的代码被放在这里
});
复制代码

是的,在模块内的代码被真正执行之前,实际上,这些代码都被包含在了一个这样的函数中。

若是你真正阅读了上一节中关于 IIFE 的内容,你会发现,其实核心思想是同样的,Node.js 对于模块私有化的实现也仍是经过了一个函数。可是这有哪些不一样呢?

虽然这里有 5 个参数,可是咱们把它们先放在一边,而后尝试站在一个模块的角度来思考这样一个问题:做为一个模块,你但愿本身具有什么样的能力呢?

  1. 暴露部分本身的方法或者变量的能力 :这是我存在的意义,由于,对于那些想使用个人人而言这是必须的。[ exports:导出对象 , module:模块的引用 ]
  2. 引入其余模块的能力:有的时候我也须要经过别人的帮助来实现一些功能,只把个人注意力放在我想作的事情(核心逻辑)上。[ require:引用方法 ]
  3. 告诉别人个人物理位置:方便别人找到我,而且对我进行更新或者修改。[ __filename:绝对文件名, __dirname:目录路径 ]

Node.js Modules 中 require 的实现

为何咱们要了解 require 方法的实现呢?由于理解这一过程,咱们能够更好地理解下面的几个问题:

  1. 当咱们引入一个模块的时候,咱们究竟作了怎样一件事情?
  2. exportsmodule.exports 有什么联系和区别?
  3. 这样的方式有什么弊端?

在文档中,有简易版的 require 的实现:

function require(/* ... */) {
  const module = { exports: {} };
  ((module, exports) => {
    // Module code here. In this example, define a function.
    // 模块代码在这里,在这个例子中,咱们定义了一个函数
    function someFunc() {}
    exports = someFunc;
    // At this point, exports is no longer a shortcut to module.exports, and
    // this module will still export an empty default object.
    // 当代码运行到这里时,exports 再也不是 module.exports 的引用,而且当前的
    // module 仍旧会导出一个空对象(就像上面声明的默认对象那样)
    module.exports = someFunc;
    // At this point, the module will now export someFunc, instead of the
    // default object.
    // 当代码运行到这时,当前 module 会导出 someFunc 而不是默认的对象
  })(module, module.exports);
  return module.exports;
}
复制代码

回到刚刚提出的问题:

1. require 作了怎样一件事情?

require 至关于把被引用的 module 拷贝了一份到当前 module 中

2. exportsmodule.exports 的联系和区别?

代码中的注释以及 require 函数第一行默认值的声明,很清楚的阐述了,exportsmodule.exports 的区别和联系:

exportsmodule.exports 的引用。做为一个引用,若是咱们修改它的值,实际上修改的是它对应的引用对象的值。

就如:

exports.a = 1
// 等同于
module.exports = {
    a: 1
}
复制代码

可是若是咱们修改了 exports 引用的地址,对于它原来所引用的内容来讲,没有任何影响,反而咱们断开了这个引用于原来的地址之间的联系:

exports = {
    a: 1
}

// 至关于

let other = {a: 1} //为了更加直观,咱们这样声明了一个变量
exports = other
复制代码

exports 从指向 module.exports 变为了 other

3. 弊端

CommonJS 这一标准的初衷是为了让 JavaScript 在多个环境下都实现模块化,可是 Node.js 中的实现依赖了 Node.js 的环境变量:moduleexportsrequireglobal,浏览器无法用啊,因此后来出现了 Browserify 这样的实现,可是这并非本文要讨论的内容,有兴趣的同窗能够读读阮一峰老师的 这篇文章

说完了服务端的模块化,接下来咱们聊聊,在浏览器这一端的模块化,又经历了些什么呢?

RequireJS & AMD(Asynchronous Module Definition)

试想一下,假如咱们如今是在浏览器环境下,使用相似于 Node.js Module 的方式来管理咱们的模块(例如 Browserify),会有什么样的问题呢?

由于咱们已经了解了 require() 的实现,因此你会发现这实际上是一个复制的过程,将被 require 的内容,赋值到一个 module 对象的属性上,而后返回这个对象的 exports 属性。

这样作会有什么问题呢?在咱们尚未完成复制的时候,没法使用被引用的模块中的方法和属性。在服务端可能这不是一个问题(由于服务器的文件都是存放在本地,而且是有缓存的),但在浏览器环境下,这会致使阻塞,使得咱们后面的步骤没法进行下去,还可能会执行一个未定义的方法而致使出错。

相对于服务端的模块化,浏览器环境下,模块化的标准必须知足一个新的需求:异步的模块管理

在这样的背景下,RequireJS 出现了,咱们简单的了解一下它最核心的部分:

  • 引入其余模块: require()
  • 定义新的模块: define()

官方文档中的使用的例子:

requirejs.config({
    // 默认加载 js/lib 路径下的module ID
    baseUrl: 'js/lib',
    // 除去 module ID 以 "app" 开头的 module 会从 js/app 路径下加载。
    // 关于 paths 的配置是与 baseURL 关联的,而且由于 paths 可能会是一个目录,
    // 因此不要使用 .js 扩展名 
    paths: {
        app: '../app'
    }
});

// 开始主逻辑
requirejs(['jquery', 'canvas', 'app/sub'],
function ($, canvas, sub) {
    //jQuery, canvas 和 app/sub 模块已经被加载而且能够在这里使用了。
});
复制代码

官方文档中的定义的例子:

// 简单的对象定义
define({
    color: "black",
    size: "unisize"
});

// 当你须要一些逻辑来作准备工做时能够这样定义:
define(function () {
    //这里能够作一些准备工做
    return {
        color: "black",
        size: "unisize"
    }
});

// 依赖于某些模块来定义属于你本身的模块
define(["./cart", "./inventory"], function(cart, inventory) {
        //经过返回一个对象来定义你本身的模块
        return {
            color: "blue",
            size: "large",
            addToCart: function() {
                inventory.decrement(this);
                cart.add(this);
            }
        }
    }
);
复制代码

优点

RequireJS 是基于 AMD 规范 实现的,那么相对于 Node.js 的 Module 它有什么优点呢?

  • 以函数的形式返回模块的值,尤为是构造函数,能够更好的实现API 设计,Node 中经过 module.exports 来支持这个,但使用 "return function (){}" 会更清晰。 这意味着,咱们没必要经过处理 “module” 来实现 “module.exports”,它是一个更清晰的代码表达式。
  • 动态代码加载(在AMD系统中经过require([],function(){})来完成)是一项基本要求。 CJS谈到了, 有一些建议,但没有彻底囊括它。 Node 不支持这种需求,而是依赖于require('')的同步行为,这对于 Web 环境来讲是不方便的。
  • Loader 插件很是有用,在基于回调的编程中,这有助于避免使用常见的嵌套大括号缩进。
  • 选择性地将一个模块映射到从另外一个位置加载,很方便的地提供了用于测试的模拟对象。
  • 每一个模块最多只能有一个 IO 操做,并且应该是简洁的。 Web 浏览器不能容忍从多个 IO 中来查找模块。 这与如今 Node 中的多路径查找相对,而且避免使用 package.json 的 “main” 属性。 而只使用模块名称,基于项目位置来简单的映射到一个位置的模块名称,不须要详细配置的合理默认规则,但容许在必要时进行简单配置。
  • 最好的是,若是有一个 "opt-in" 能够用来调用,以便旧的 JS 代码能够加入到新系统。

若是一个 JS 模块系统没法提供上述功能,那么与 AMD 及其相关 API 相比,它将在回调需求,加载器插件和基于路径的模块 ID 等方面处于明显的劣势。

新的问题

经过上面的语法说明,咱们会发现一个很明显的问题,在使用 RequireJS 声明一个模块时,必须指定全部的依赖项 ,这些依赖项会被当作形参传到 factory 中,对于依赖的模块会提早执行(在 RequireJS 2.0 也能够选择延迟执行),这被称为:依赖前置。

这会带来什么问题呢?

加大了开发过程当中的难度,不管是阅读以前的代码仍是编写新的内容,也会出现这样的状况:引入的另外一个模块中的内容是条件性执行的。

SeaJS & CMD(Common Module Definition)

针对 AMD 规范中能够优化的部分,CMD 规范 出现了,而 SeaJS 则做为它的具体实现之一,与 AMD 十分类似:

// AMD 的一个例子,固然这是一种极端的状况
define(["header", "main", "footer"], function(header, main, footer) { 
    if (xxx) {
      header.setHeader('new-title')
    }
    if (xxx) {
      main.setMain('new-content')
    }
    if (xxx) {
      footer.setFooter('new-footer')
    }
});

 // 与之对应的 CMD 的写法
define(function(require, exports, module) {
    if (xxx) {
      var header = require('./header')
      header.setHeader('new-title')
    }
    if (xxx) {
      var main = require('./main')
      main.setMain('new-content')
    }
    if (xxx) {
      var footer = require('./footer')
      footer.setFooter('new-footer')
    }
});
复制代码

咱们能够很清楚的看到,CMD 规范中,只有当咱们用到了某个外部模块的时候,它才会去引入,这回答了咱们上一小节中遗留的问题,这也是它与 AMD 规范最大的不一样点:CMD推崇依赖就近 + 延迟执行

仍然存在的问题

咱们可以看到,按照 CMD 规范的依赖就近的规则定义一个模块,会致使模块的加载逻辑偏重,有时你并不知道当前模块具体依赖了哪些模块或者说这样的依赖关系并不直观。

并且对于 AMD 和 CMD 来讲,都只是适用于浏览器端的规范,而 Node.js module 仅仅适用于服务端,都有各自的局限性。

ECMAScript6 Module

ECMAScript6 标准增长了 JavaScript 语言层面的模块体系定义,做为浏览器和服务器通用的模块解决方案它能够取代咱们以前提到的 AMDCMD ,CommonJS。(在此以前还有一个 UMD(Universal Module Definition)规范也适用于先后端,可是本文不讨论,有兴趣能够查看 UMD文档 )

关于 ES6 的 Module 相信你们天天的工做中都会用到,对于使用上有疑问能够看看 ES6 Module 入门,阮一峰,固然你也能够查看 TC39的官方文档

为何要在标准中添加模块体系的定义呢?引用文档中的一句话:

"The goal for ECMAScript 6 modules was to create a format that both users of CommonJS and of AMD are happy with"

"ECMAScript 6 modules 的目标是创造一个让 CommonJS 和 AMD 用户都满意的格式"

它凭借什么作到这一点呢?

  • 与 CommonJS 同样,具备紧凑的语法,对循环依赖以及单个 exports 的支持。
  • 与 AMD 同样,直接支持异步加载和可配置模块加载。

除此以外,它还有更多的优点:

  • 语法比CommonJS更紧凑。
  • 结构能够静态分析(用于静态检查,优化等)。
  • 对循环依赖的支持比 CommonJS 好。

注意这里的描述里出现了两个词 循环依赖静态分析,咱们在后面会深刻讨论。首先咱们来看看, TC39 的 官方文档 中定义的 ES6 modules 规范是什么。

深刻 ES6 Module 规范

15.2.1.15 节 中,定义了 Abstract Module Records (抽象的模块记录) 的 Module Record Fields (模块记录字段) 和 Abstract Methods of Module Records (模块记录的抽象方法)

Module Record Fields 模块记录字段

Field Name(字段名) Value Type(值类型) Meaning(含义)
[[Realm]] 域 Realm Record | undefined The Realm within which this module was created. undefined if not yet assigned.

将在其中建立当前模块,若是模块未声明则为 undefined。

[[Environment]] 环境 Lexical Environment | undefined The Lexical Environment containing the top level bindings for this module. This field is set when the module is instantiated.

词法环境包含当前模块的顶级绑定。 在实例化模块时会设置此字段。

[[Namespace]] 命名空间 Object | undefined The Module Namespace Object if one has been created for this module. Otherwise undefined.

模块的命名空间对象(若是已为此模块建立了一个)。 不然为 undefined。

[[Evaluated]] 执行结束 Boolean Initially false, true if evaluation of this module has started. Remains true when evaluation completes, even if it is an abrupt completion

初始值为 false 当模块开始执行时变成 true 而且持续到执行结束,哪怕是忽然的终止(忽然的终止,会有不少种缘由,若是对缘由感兴趣能够看下 这个回答)

Abstract Methods of Module Records 模块记录的抽象方法

Method 方法 Purpose 目的
GetExportedNames(exportStarSet) Return a list of all names that are either directly or indirectly exported from this module.

返回一个今后模块直接或间接导出的全部名称的列表。

ResolveExport(exportName, resolveSet, exportStarSet)

Return the binding of a name exported by this modules. Bindings are represented by a Record of the form {[[module]]: Module Record, [[bindingName]]: String}.

返回此模块导出的名称的绑定。 绑定由此形式的记录表示:{[[module]]: Module Record, [[bindingName]]: String}

ModuleDeclarationInstantiation()

Transitively resolve all module dependencies and create a module Environment Record for the module.

传递性地解析全部模块依赖关系,并为模块建立一个环境记录

ModuleEvaluation()

Do nothing if this module has already been evaluated. Otherwise, transitively evaluate all module dependences of this module and then evaluate this module.

若是此模块已经被执行过,则不执行任何操做。 不然,传递执行此模块的全部模块依赖关系,而后执行此模块。

ModuleDeclarationInstantiation must be completed prior to invoking this method.

ModuleDeclarationInstantiation 必须在调用此方法以前完成

也就是说,一个最最基础的模块,至少应该包含上面这些字段,和方法。反复阅读后你会发现,其实这里只是告知了一个最基础的模块,应该包含某些功能的方法,或者定义了模块的格式,可是在咱们具体实现的时候,就像原文中说的同样:

An implementation may parse a sourceText as a Module, analyze it for Early Error conditions, and instantiate it prior to the execution of the TopLevelModuleEvaluationJob for that sourceText.

实现能够是:将 sourceText 解析为模块,对其进行早期错误条件分析,并在执行TopLevelModuleEvaluationJob以前对其进行实例化。

An implementation may also resolve, pre-parse and pre-analyze, and pre-instantiate module dependencies of sourceText. However, the reporting of any errors detected by these actions must be deferred until the TopLevelModuleEvaluationJob is actually executed.

实现还能够是:解析,预解析和预分析,并预先实例化 sourceText 的模块依赖性。 可是,必须将这些操做检测到的任何错误,推迟到实际执行TopLevelModuleEvaluationJob 以后再报告出来。

经过这些咱们只能得出一个结论,在具体实现的时候,只有第一步是固定的,也就是:

解析:如 ParseModule 这一节中所介绍的同样,首先会对模块的源代码进行语法错误检查。例如 early-errors,若是解析失败,让 body 报出一个或多个解析错误和/或早期错误。若是解析成功而且没有找到早期错误,则将 body 做为生成的解析树继续执行,最后返回一个 Source Text Module Records

那后面会发生什么呢?咱们能够经过阅读具体实现的源码来分析。

从 babel-helper-module-transforms 来看 ES6 module 实现

Babel 做为 ES6 官方指定的编译器,在现在的前端开发中发挥着巨大的做用,它能够帮助咱们将开发人员书写的 ES6 语法的代码转译为 ES5 的代码而后交给 JS 引擎去执行,这一行为让咱们能够毫无顾忌的使用 ES6 给咱们带来的方便。

这里咱们就以 Babel 中 babel-helper-module-transforms 的具体实现,来看看它是如何实现 ES6 module 转换的步骤

在这里我不会逐行的去分析源码,而是从结构和调用上来看具体的逻辑

首先咱们罗列一下这个文件中出现的全部方法(省略掉方法体和参数)

/** * Perform all of the generic ES6 module rewriting needed to handle initial * module processing. This function will rewrite the majority of the given * program to reference the modules described by the returned metadata, * and returns a list of statements for use when initializing the module. * 执行处理初始化所需的全部通用ES6模块重写 * 模块处理。 这个函数将重写给定的大部分 * 程序引用返回的元数据描述的模块, * 并返回初始化模块时使用的语句列表。 */
export function rewriteModuleStatementsAndPrepareHeader() {...}

/** * Flag a set of statements as hoisted above all else so that module init * statements all run before user code. * 将一组语句标记为高于其余全部语句,以便模块初始化  * 语句所有在用户代码以前运行。 */
export function ensureStatementsHoisted() {...}
/** * Given an expression for a standard import object, like "require('foo')", * wrap it in a call to the interop helpers based on the type. * 给定标准导入对象的表达式,如“require('foo')”,  * 根据类型将其包装在对 interop 助手的调用中。 */
export function wrapInterop() {...}

/** * Create the runtime initialization statements for a given requested source. * These will initialize all of the runtime import/export logic that * can't be handled statically by the statements created by * 为给定的请求源建立运行时初始化语句。  * 这些将初始化全部运行时导入/导出逻辑  * 不能由建立的语句静态处理 * buildExportInitializationStatements(). */
export function buildNamespaceInitStatements() {...}


/** * Build an "__esModule" header statement setting the property on a given object. * 构建一个“__esModule”头语句,在给定对象上设置属性 */
function buildESModuleHeader() {...}


/** * Create a re-export initialization loop for a specific imported namespace. * 为特定导入的命名空间,建立 从新导出 初始化循环。 */
function buildNamespaceReexport() {...}
/** * Build a statement declaring a variable that contains all of the exported * variable names in an object so they can easily be referenced from an * export * from statement to check for conflicts. * 构建一个声明,声明包含对象中全部导出变量名称的变量的语句,以即可以从export * from语句中轻松引用它们以检查冲突。 */
function buildExportNameListDeclaration() {...}

/** * Create a set of statements that will initialize all of the statically-known * export names with their expected values. * 建立一组将经过预期的值来初始化 全部静态已知的导出名的语句 */
function buildExportInitializationStatements() {...}

/** * Given a set of export names, create a set of nested assignments to * initialize them all to a given expression. * 给定一组 export names,建立一组嵌套分配将它们所有初始化为给定的表达式。 */
function buildInitStatement() {...}

复制代码

而后咱们来看看他们的调用关系:

咱们以 A -> B 的形式表示在 A 中调用了 B

  1. buildNamespaceInitStatements:为给定的请求源建立运行时初始化语句。这些将初始化全部运行时导入/导出逻辑

  2. rewriteModuleStatementsAndPrepareHeader 全部通用ES6模块重写,以引用返回的元数据描述的模块。
    -> buildExportInitializationStatements建立全部静态已知的名称的 exports
    -> buildInitStatement 给定一组 export names,建立一组嵌套分配将它们所有初始化为给定的表达式。

因此总结一下,加上前面咱们已知的第一步,其实后面的步骤分为两部分:

  1. 解析:首先会对模块的源代码进行语法错误检查。例如 early-errors,若是解析失败,让 body 报出一个或多个解析错误和/或早期错误。若是解析成功而且没有找到早期错误,则将 body 做为生成的解析树继续执行,最后返回一个 Source Text Module Records
  2. 初始化全部运行时导入/导出逻辑
  3. 以引用返回的元数据描述的模块,而且用一组 export names 将全部静态的 exports 初始化为指定的表达式。

到这里其实咱们已经能够很清晰的知道,在 编译阶段 ,咱们一段 ES6 module 中的代码经历了什么:

ES6 module 源码 -> Babel 转译-> 一段能够执行的代码

也就是说直到编译结束,其实咱们模块内部的代码都只是被转换成了一段静态的代码,只有进入到 运行时 才会被执行。

这也就让 静态分析 有了可能。

最后

本文咱们从 JavaScript Module 的发展史开始聊起,一直聊到了现在与咱们息息相关的 ES6 代码的编译,很感谢前人走出的这些道路,让现在我这样的普通人也可以进入到编程的世界,也不得不感叹,一个问题越深究,才会发现其中并不简单。

感谢那些可以耐心读到这里的人,由于这篇文章前先后后,也花了4天的时间来研究,时常感叹有价值的资料实在太少了。

下一篇咱们会接着聊聊静态分析,和循环引用

我是 Dendoink ,奇舞周刊原创做者,掘金 [联合编辑 / 小册做者] 。

对于技术人而言: 是单兵做战能力, 则是运用能力的方法。驾轻就熟,出神入化就是 。在前端娱乐圈,我想成为一名出色的人民艺术家。

扫一扫关注公众号 [ 前端恶霸 ] ,我在这里等你:

相关文章
相关标签/搜索