從 IIFE 聊到 Babel 帶你深入瞭解前端模組化發展體系

dendoink發表於2019-04-23

前言

作為一名前端工程師,每天的清晨,你走進公司的大門,回味著前臺妹子的笑容,摘下耳機,泡上一杯茶,開啟 Terminal 進入對應的專案目錄下,然後 npm run start / dev 或者 yarn start / dev 就開始了一天的工作。

當你需要進行時間的轉換隻需要使用 dayjs 或者 momentjs, 當你需要封裝 http 請求的時候,你可以用 fetch 或者 axios, 當你需要做資料處理的時候,你可能會用 lodash 或者 underscore

不知道你有沒有意識到,對於今天的我們而言,這些工具包讓開發效率得到了巨大的提升,但是這一切是從什麼開始的呢?

這些就要從 Modular design (模組化設計) 說起:

Modular design (模組化設計)

在我剛接觸前端的時候,經常聽說 Modular design (模組化設計) 這樣的術語,面試時也會經常被問到,“聊聊前端的模組化”這樣的問題,或許很多人都可以說出幾個熟悉的名詞,甚至是他們之間的區別:

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

但就像你閱讀一個專案的原始碼一樣,如果從第一個 commit 開始研究,那麼你能收穫的或許不僅僅是,知道他們有什麼區別,更重要的是,能夠知道在此之前的歷史中,是什麼樣的原因,導致了區別於舊的規範而產生的新規範,並且基於這些,或許你能夠從中體會到這些改變意味著什麼,甚至在將來的某個時刻,你也能成為這規則的制定者之一

所以讓我們回到十年前,來看看是怎麼實現模組化設計的:

IIFE

IIFE 是 Immediately Invoked Function Expression 的縮寫,作為一個基礎知識,很多人可能都已經知道 IIFE 是怎麼回事,(如果你已經掌握了 IIFE,可以跳過這節閱讀後面的內容) 但這裡我們仍舊會解釋一下,它是怎麼來的,因為在後面我們還會再次提到它:

最開始,我們對於模組區分的概念,可能是從檔案的區分開始的,在一個簡易的專案中,程式設計的習慣是通過一個 HTML 檔案加上若干個 JavaScript 檔案來區分不同的模組,就像這樣:

從 IIFE 聊到 Babel 帶你深入瞭解前端模組化發展體系

我們可以通過這樣一個簡單的專案來說明,來看看每個檔案裡面的內容:

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 環境下的全域性變數) 物件上,嘗試去列印驗證一下:

從 IIFE 聊到 Babel 帶你深入瞭解前端模組化發展體系

這簡直就是一場噩夢,你可能沒有意識到這會導致什麼嚴重的結果,我們試著在 footer.js 中對 header 變數進行賦值操作,讓我們在末尾加上這樣一行程式碼:

header = 'nothing'
複製程式碼

列印後你就會發現,window.header 的已經被更改了:

從 IIFE 聊到 Babel 帶你深入瞭解前端模組化發展體系

試想一下,你永遠無法預料在什麼時候什麼地點無意中就改掉了之前定義的某個變數,如果這是在一個團隊中,這是一件多麼可怕的事情。

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 聊到 Babel 帶你深入瞭解前端模組化發展體系

這個方案還不夠完美,怎麼改進呢?

答案就是我們要說的 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 是一套通用的規範,與之對應的有非常多不同的實現:

從 IIFE 聊到 Babel 帶你深入瞭解前端模組化發展體系

圖片來源於 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 ,奇舞週刊原創作者,掘金 [聯合編輯 / 小冊作者] 。

對於技術人而言: 是單兵作戰能力, 則是運用能力的方法。得心應手,出神入化就是 。在前端娛樂圈,我想成為一名出色的人民藝術家。

掃一掃關注公眾號 [ 前端惡霸 ] ,我在這裡等你:

從 IIFE 聊到 Babel 帶你深入瞭解前端模組化發展體系

相關文章