ES modules

注意:這篇文章講的是正經的es module規範 及瀏覽器的實現!webpack項目中es module會被parse成commonjs,和這個沒大關係!webpack

 

總結:web

ES模塊加載的主要過程:算法

  1. 構造 —— 尋找,下載並解析全部文件成模塊記錄瀏覽器

  2. 實例化 —— 在內存中尋找位置存放全部導出的值(可是暫時還不要給他們填上具體的值)而後讓導出和導入都指向這些內存中的位置。這個過程也叫作連接(linking)。緩存

  3. 求值 —— 執行編碼並給實例化中所對應的內存的位置填充實際的值。服務器

 

 

ESmodules與CommonJS規範   modlue 實現的最大區別是: 網絡

 

構建階段單獨劃分出來容許瀏覽器在處理同步的實例化階段以前就可以下載文件而且構建模塊依賴圖。數據結構

 

CommonJS 不採用這用的方式是由於從文件系統中讀取文件和從網絡中下載文件比起來要快得多。這意味着 Node 能夠在加載文件的時候阻塞主線程。而後既然文件以及加載完了,那麼就順其天然地實例化和求值(在 CommonJS 中不是分開的階段)。這也意味着,在返回模塊實例以前,須要遍歷整棵模塊依賴樹並對任何模塊依賴加載、實例化和求值。併發

 

 

 

 

原文:異步

 

(下文轉)

編者按:本文由 Mactavish 翻譯並發表於衆成翻譯

雖然花了近十年的標準化工做才走到這一步,ES 模塊終於爲 JavaScript 帶來了正式的,標準化的模塊系統。

漫長的等待終於要結束了,隨着即將在五月發佈的 Firefox 60 (目前尚處於 beta 版本中),全部的主流瀏覽器都即將支持 ES 模塊,而且 Node 模塊工做小組目前也正在爲 Node.js 添加對 ES 模塊的支持。同時,ES 模塊對 WebAssembly 的支持也正在進行當中。

許多 JavaScript 的開發者都知道 ES 模塊一直存在着一些爭議,可是不多有人真正地知道 ES 模塊的原理。

如今就讓咱們來探索一下 ES 模塊到底解決了什麼問題以及它和其餘模塊系統的區別。

模塊到底解決了什麼問題?

仔細想一想,使用 JavaScript 編碼在於正確地管理變量,在於給變量賦值,或者給變量賦以數值或者合併兩個變量並把它們賦值給另一個變量。

 

由於你的大多數代碼都是在更改變量,如何組織這些變量將會對你的編碼方式以及代碼的維護產生重大的影響。

當一次只須要考慮幾個變量的時候使得事情變得很是簡單,JavaScript 有一個方式來幫助你實現這個目標,那就是 —— 做用域。由於做用域的存在,函數不能訪問 定義在其餘函數內部的變量。

 

這很棒。這意味着當你專一於實現一個函數的時候,你只須要專一於實現這個函數,而不須要擔憂其餘的函數會影響到你這個函數裏的變量。

不過,它也有一個缺陷,它使得不一樣的函數之間共享變量變得更加困難。

那麼假如你的確想要在做用域以外共享你的變量呢?一般的作法是將它放在當前做用域之上,好比:全局做用域。

或許你還記得使用 jQuery 的那些日子,在你加載任何 jQuery 的插件以前,你必須確保 jQuery 已經存在於全局做用域內了。

這是可行的,可是會產生一些煩人的問題。

首先,你全部的 script 標籤都必須放置於一個正確的順序。那麼你就必須很當心並確保這些腳本之間不會互相影響。

若是你確實不當心搞亂了順序,那麼在代碼運行的時候,你的應用就會拋出異常。當函數尋找 jQuery 對象的存在 —— 也就是全局做用域之下,可是卻找不到的時候,函數就會報錯並中止執行。

這讓代碼維護變得棘手。移除舊的代碼或者是 script 標籤就像是玩賭場轉盤同樣。你沒法預料到什麼代碼可能崩潰。代碼之間的依賴關係變得隱蔽。任何函數均可以獲取到全局做用域上的任何東西,因此你並無辦法知道哪一個函數依賴於哪一個 script 標籤。

其次,因爲你的變量都存在於全局做用域上,全部處於這個做用域之上的代碼均可以改變這些變量。惡意代碼能夠經過更改這些變量來讓你的代碼作並不是你本意的事情,或者非惡意的代碼會不當心破壞你的變量。

模塊如何提供幫助

模塊爲你提供了一個更加好的方式來組織這些變量和方法。有了模塊,你能夠將這些有意義的函數和變量組織在一塊兒。

模塊會將這些函數和變量放入一個模塊做用域當中。模塊做用域使得模塊中的不一樣函數可以共享這些變量。

可是不一樣與函數做用域,模塊做用域有一種方法可以使得其餘的模塊也能夠訪問這個模塊的變量。他們能夠顯式地指定模塊中的哪些變量,類或者是函數能夠被其餘模塊訪問。

當一些東西對其餘模塊可用的時候,這叫作 "導出(export)"。當模塊的導出存在的時候,其餘模塊就可以顯式地指定它們依賴於這個模塊的某些變量,類或者函數。

由於存在這種顯式的關係,你能夠明確的指出當你去掉了另一個(導出),哪一個模塊會崩潰掉。

一旦擁有了這種能在模塊之間導出和導入變量的能力,把你的代碼分割成更小而且可以互相之間獨立工做的代碼塊就變得很容易了。 而後你就能夠結合或者重組這些代碼塊,像組合樂高積木同樣,來使用一樣的模塊建立不一樣的應用。

正由於模塊如此地有用,已經存在不少給 JavaScript 添加模塊的嘗試。目前,有兩種模塊系統被普遍地使用着。CommonJS(CJS) 曾經被 Node.js 所使用。ESM(ECMAScript 模塊)是一個更新的模塊系統,並加入到 JavaScript 的規範當中。瀏覽器已經支持 ES 模塊了,Node.js 也正在添加對它的支持。

如今,就讓咱們更加深刻地來看一下這個新的模塊系統是如何運做的。

ES 模塊是如何運做的

當使用模塊來開發的時候,會創建一個模塊模塊依賴圖。不一樣依賴之間聯繫來自於你使用的任何 import 語句。

這些 import 語句是瀏覽器或者 Node 確切地知道你須要加載什麼樣的代碼的關鍵之處。你須要提供一個文件來做爲依賴圖的入口。 從這個入口開始,根據這些 import 語句就能夠找剩餘所須要的代碼。

可是瀏覽器並不能直接使用這些文件自己。它必需要通過解析並轉換成一種叫作 "模塊記錄(Module Records)"的數據結構。只有這樣,瀏覽器才能確切地知道這個文件裏發生了什麼。

在這以後,模塊記錄須要轉變成模塊實例。模塊實例包含了兩個要素:編碼(code)狀態(state)

編碼基本上就是一些系列的指令。它就像配方同樣。可是隻有配方自己,什麼都作不了,因此還須要一些原材料來配合這些指令。

什麼是狀態?狀態就提供了這些原材料。狀態就是這些變量在任什麼時候間點的具體值。固然,這些變量不過是內存中保存這些變量的容器的別名。

因此模塊實例就結合了編碼(一系列的指令)和狀態(全部的變量的值)。

咱們須要的是每個模塊的模塊實例。模塊加載的過程就是從入口文件開始最後獲得整個模塊實例的依賴圖。

對於 ES 模塊來講,這個過程主要分三步來進行:

  1. 構造 —— 尋找,下載並解析全部文件成模塊記錄

  2. 實例化 —— 在內存中尋找位置存放全部導出的值(可是暫時還不要給他們填上具體的值)而後讓導出和導入都指向這些內存中的位置。這個過程也叫作連接(linking)。

  3. 求值 —— 執行編碼並給實例化中所對應的內存的位置填充實際的值。

人們說 ES 模塊是異步的。你能夠認爲它是異步的由於實際的運做被分紅了三個不一樣的階段 —— 加載,實例化以及求值,而這些階段均可以分開完成。

這意味着規範確實引入了一種在 CommonJS 中沒有的異步。稍後我會做更多的解釋,可是在 CJS 中,一個模塊下游的依賴關係是當即加載,實例化並求值的,不存在任何的間斷。

可是,這些步驟不必定要是異步的,它們也能夠以同步的方式完成。這取決於用什麼來加載。這是由於不是全部的東西都是由 ES 模塊規範來定義的。實際上有兩部分工做,分別由不一樣的規範來覆蓋。

ES 模塊規範 說明了應該如何將文件解析成模塊記錄,以及如何實例化和對模塊求值。然而,它並無指明如何獲取這個模塊。

加載文件的是模塊加載器(loader), 而加載器由不一樣的規範來指定。對於瀏覽器來講,這個規範就是 HTML 規範。可是,基於你使用的不一樣的平臺能夠有不一樣的加載器。

加載器同時還精準地控制着模塊的加載方式。這些方法叫作 ES 模塊方法 —— ParseModule, Module.Instantiate 以及 Module.Evaluate。這有點像一個提線木偶操做師操做着 JS 引擎。

如今讓咱們更加詳細地介紹每一步的過程。

構造(Construction)

對於每個模塊,在構造過程都會經歷這三個過程:

  1. 找到在哪裏下載包含該模塊的文件(也稱做模塊解析)

  2. 獲取文件(經過 url 從文件系統中下載)

  3. 將這些文件解析成模塊記錄

尋找並獲取文件

加載器會處理查找和下載文件的過程,首先它須要找到入口文件。在 HTML 當中,你經過 script 標籤來告訴加載器哪裏去查找。

可是,它如何找到接下來的一堆模塊呢 —— main.js 直接依賴的模塊。

這就是 import 語句發揮做用的地方。import 語句的其中一部分叫作模塊標識符,它會告訴加載器去哪尋找下一個模塊。

關於模塊標識符值得一提的地方是:有時候它們須要在瀏覽器和 Node 之間作不一樣的處理。每一個宿主都有各自的方法來解析模塊標識符的字符串。爲了達到這個目的,它使用了名爲模塊解析算法的東西,而這個算法根據平臺的不一樣也有所不一樣。目前來講,一些在 Node 中可以正常解析的模塊標識符沒法在瀏覽器中正常解析,可是一個致力於修復它的工做正在進行中。

直到這個問題被修復以前,瀏覽器只接受 URL 做爲模塊標識符。它們會從模塊標識符指定的 URL 中下載對應的模塊文件。對於模塊的依賴圖的生成來講,這一步不是同時進行的。由於在你解析模塊文件以前,你沒法明確該模塊須要什麼依賴,而且你沒法在獲取文件以前就解析它。

這意味着咱們必須一層一層地深刻模塊的結構樹,解析一個文件而後理清楚改模塊的依賴,而後進一步獲取並加載這些依賴。

若是主線程須要等等待每一個文件的下載,那麼其餘的下載任務就會在隊列中等待。

這是由於在瀏覽器當中,下載的部分須要花費很長的時間。

基於這張 圖表.

像這樣阻塞主線程會使得那些使用模塊來構建的 App 的速度變得很慢。這也是 ES 模塊規範將實現算法劃分紅多個階段的緣由之一。將構建階段單獨劃分出來容許瀏覽器在處理同步的實例化階段以前就可以下載文件而且構建模塊依賴圖。

這種實現方式 —— 將模塊算法劃分爲不一樣階段,是 CommonJS 模塊和 ES 模塊的主要區別之一。

CommonJS 不採用這用的方式是由於從文件系統中讀取文件和從網絡中下載文件比起來要快得多。這意味着 Node 能夠在加載文件的時候阻塞主線程。而後既然文件以及加載完了,那麼就順其天然地實例化和求值(在 CommonJS 中不是分開的階段)。這也意味着,在返回模塊實例以前,須要遍歷整棵模塊依賴樹並對任何模塊依賴加載、實例化和求值。

CommonJS 的實現方式有一些影響,以後我會進行解釋。但值得注意的一點是,在使用 CommonJS 模塊的 Node 環境中,你能夠在模塊標識符中使用變量。由於在查找下一個模塊以前,會執行當前模塊的全部代碼(require 語句以前)。這意味着當 Node 進行模塊解析的時候,模塊標識符中的變量已經有值了。

可是對於 ES 模塊來講,咱們在執行任何求值計算以前,事先構建了整個模塊依賴圖。這意味着在模塊標識符當中不能夠存在變量,由於這些變量尚未具體的值。

可是有時候,在模塊路徑中使用變量很是有用。好比,你可能會根據代碼的不一樣條件或者當前運行環境的不一樣來切換加載不一樣的模塊。

爲了在 ES 模塊中實現一樣的效果,這裏有一個叫作 dynamic import 的提案。有了它,你可使用相似 import(`${path}/foo.js`) 的語句

這個方法的原理在於任何使用 import() 加載的文件會被處理爲一個分散的依賴圖的入口。而被動態引入的模塊創建了一個新的模塊依賴圖,這個模塊依賴圖的處理是分開進行的。

有一點須要注意 —— 同時處於兩個依賴圖中的任何一個模塊將會共享同一個模塊實例。這是由於模塊加載器會對模塊實例進行緩存。對於特定全局做用域中的每個模塊,都只會擁有一個模塊實例。

這意味着引擎的工做量更少。好比,一個模塊文件只會獲取一次,即便有多個模塊同時依賴於它(這是咱們須要模塊緩存的緣由之一,咱們會在求值的章節中介紹另一個緣由)。

加載器經過一個叫作 模塊映射(module map) 的東西來管理模塊緩存。每個全局環境都在一個單獨的模塊映射裏跟蹤其模塊。

當加載器加載一個 URL 的時候,它會將這個 URL 放到模塊映射裏,而後記下當前處於加載狀態。而後它會發出請求並開始處理下一個要加載的文件。

那麼,若是另一個模塊依賴了某個相同的文件呢?加載器會檢查模塊映射裏的每個 URL,若是它發現已經處於加載狀態了,那麼加載器會跳過並處理下一個 URL。

可是,模塊映射不只僅跟蹤那些模塊正在被加載,它還做爲模塊的緩存,咱們接下來會講到。

解析(Parsing)

如今咱們已經獲取到了模塊文件,咱們須要將它解析成模塊記錄。這可以幫助瀏覽器理解模塊的各部分分別是什麼。

一旦模塊記錄被建立,就會被放到模塊映射裏。這意味着,只要從外部請求對應的模塊,模塊加載器就能夠從模塊映射中拿到對應的模塊記錄。

在解析的過程當中,有一個細節看起來可能微不足道,可是實際上有很大的影響。全部的模塊都被以有 "use strict" 在頂部的狀況解析。還有一些其餘細微的差異,好比,await 關鍵字被保留在模塊的頂級代碼中,以及 this 的值是 undefined

解析的不一樣被稱之爲 解析目標(parsed goal),若是對同一個文件使用不一樣的解析目標,那就會的到不一樣的結果。因此你須要在解析開始以前就知道文件的類型,不管它是模塊與否。

在瀏覽器當中很是簡單,你只須要在 script 標籤中加入 type=module。這告訴瀏覽器,當前文件須要以模塊的方式來解析,而且由於只有模塊纔可以被導入,瀏覽器就可以知道當前文件裏全部的導入也都是模塊。

可是在 Node 當中,咱們不使用 HTML 標籤,因此咱們沒辦法使用 type 屬性。社區嘗試解決這個問題的方法之一是 —— 使用 .mjs 擴展名。使用這個擴展名能告訴 Node,「當前這個文件是一個模塊」。因此你能看到在談論使用這個來做爲解析目標的信號。關於這個方案的討論還在不斷地進行中,因此目前來講 Node 社區最後到底會使用什麼樣的信號還不明確。

不管使用哪一種方式,加載器都會決定是否要將一個文件當作模塊來解析。若是它是一個模塊,而且有一些導入,那麼加載器將會再次進行這個過程,直到全部的文件都被加載和解析。

當咱們完成以後,也就是加載的過程結束以後,你已經從原來只有一個入口文件的變成擁有一堆模塊記錄。

下一步就是實例化這些模塊並將這些實例連接在一塊兒。

實例化(Instantiation)

像我以前所提到的,一個模塊實例包含了編碼和狀態。這些狀態存在於內存之中,所以實例化的步驟就是將模塊的內容鏈接到內存之中。

首先,JS 引擎會建立一個模塊環境記錄。它爲模塊記錄管理變量,而後引擎會爲模塊導出在內存中找到位置存放。模塊環境變量會跟蹤內存中的哪一個位置對應哪一個導出。

這些內存中的位置暫時尚未具體的值在裏面。只有在求值以後纔會爲它們填充實際的值。對於這個規則有個要警戒的地方:任何導出的函數聲明都會在這個階段初始化。這對於求值來講更加簡單。

爲了實例化模塊整個依賴圖,引擎會執行 "深度優前後序遍歷",這意味着引擎會深刻到依賴圖的底部 —— 到底部某個不依賴任何其餘依賴的模塊依賴,而後設置它們的導出。

當引擎完成連接模塊下游的全部導出的時候,而後回到上一個級別連接那個來自模塊全部的導入。

注意,導入和導出都指向內存中的同一個位置。連接全部的導出首先確保了全部的導入都可以正確地匹配這些導出。

這個過程和 CommonJS 模塊是不一樣的。在 CommonJS 當中,整個導出對象被複制到導出上。這意味着任何的導出值(好比數字)都是副本。

這也表示,若是導出模塊發生以後發生了改變,導入模塊並不會觀測到這個變化。

與之相反,ES 模塊使用了叫作 「活動綁定(live bindings)」 的機制。導入和導出模塊都指向內存中的同一個位置。當導出模塊改變了其中導出的某個值,這個變化也會迅速地顯如今導入模塊當中。

導出值的模塊能夠隨時更改這些值,但導入模塊不能更改其導入的值。也就是說,若是一個模塊導入了一個對象,它能夠改變對象身上的屬性值。

使用活動綁定的緣由是能夠在不運行任何代碼的的狀況下就將這些模塊連接在一塊兒。這一點對於循環依賴的模塊的執行頗有幫助,接下來我會解釋。

因此在這一步的最後,咱們將全部模塊實例的導入和導出的變量在內存中的位置連接在一塊兒。

如今咱們就能夠開始對代碼求值,而後給這些內存中的地址填充上實際的值。

Evaluation

最後一步就是對這些內存中的位置進行值的填充。JS 引擎經過執行頂層代碼(函數以外的代碼)來實現。

除了給內存裏的空間填值以外,求值過程也存在着一些反作用,好比,一個模塊可能會請求一個服務器。

由於潛在的反作用,你只想對模塊求值一次。與實例化中發生的連接相反,它們屢次連接的結果都是一致的,但根據操做次數的不一樣,求值所產生的結果可能不一樣。

這也是須要模塊映射的緣由之一。模塊映射經過規範的 URL 來緩存模塊,使得每一個模塊只有一個模塊記錄。這保證了每一個模塊只會執行一次,和實例化同樣,這個過程也是以 「深度優前後續遍歷」 的方式來執行。

那麼咱們以前提到的循環引用的問題呢?

在循環依賴之中,最終會在依賴圖中產生一個循環。一般來講,這會是一個長循環,可是爲了解釋這個問題,我打算用一個模擬的短循環的例子。

首先咱們來看看在 CommonJS 模塊中,循環依賴是怎麼實現的。首先,main 模塊會一直執行到 require 語句。而後它就會轉而加載 counter 模塊。

而後 counter 模塊會嘗試從導出對象上訪問 message。但由於 message 在 main 模塊中尚未被求值,會返回 undefined。JS 引擎將會爲本地變量分配內存空間並設置其值爲 undefined

求值過程將會繼續在 counter 模塊的的頂級代碼中執行,並執行到底部。咱們想看看 message 最終是否會獲取到正確的值(在 main.js 求值以後),因此咱們設置了一個 timeout。而後求值將在 main.js 中恢復。

 

message 變量將會初始化並被加到內存當中。可是由於 main 導出的 message 和 counter 中的 require 尚未關聯,因此在引入的模塊中(counter.js),message 仍然保持爲 undefined

 

若是導出都是以 "活動綁定" 的方式處理的,那麼 counter 模塊最終將會獲得正確的值。那麼等到 timeout 運行的時候,main.js 求值已經結束而且填充了值。

支持這些循環依賴的背後是 ES 模塊設計的重要原理。是這個「三步驟」的設計才使得循環依賴得以實現。

ES 模塊的當前狀態

隨着 Firefox 60 在五月早期的發佈,全部主流瀏覽器都將默認支持 ES 模塊了。Node 也添加了對其的支持,而且有一個致力於解決 CommonJS 和 ES 模塊之間的兼容性的問題的工做小組在不斷地努力。

這意味着,你將可使用 type=module 的方式來使用模塊的導入和導出。然而,更多的模塊特性也即將到來。處於 Stage 3 的提案 dynamic import 也在具體的進程中。import.meta 也是如此,它將支持 Node 上的一些用例。同時 module resolution 提案也會使得在瀏覽器和 Node.js 之間的差異變得更加平滑細微。因此咱們可以期待將來能夠鞥更好地使用模塊。

 

2018年5月6日

相關文章
相關標籤/搜索