js模塊:深度學(xué)習(xí)

ES模塊將正式的標(biāo)準(zhǔn)化模塊系統(tǒng)引入JavaScript。但是,花了近十年的標(biāo)準(zhǔn)化工作才能到達(dá)這里。

但是等待幾乎結(jié)束了。隨著5月份Firefox 60的發(fā)布(目前處于beta版),所有主要的瀏覽器都將支持ES模塊,而Node模塊工作組目前正在努力為Node.js添加ES模塊支持。而且,用于WebAssembly的ES模塊集成也在進(jìn)行中。

許多JavaScript開發(fā)人員都知道ES模塊一直存在爭(zhēng)議。但是實(shí)際上很少有人了解ES模塊的工作方式。

讓我們看一下ES模塊要解決的問題以及它們與其他模塊系統(tǒng)中的模塊有何不同。

模塊可以解決什么問題?

你可以認(rèn)為使用JavaScript進(jìn)行編碼就完全是管理變量。這都是關(guān)于為變量分配值,或向變量添加數(shù)字,或?qū)蓚€(gè)變量組合在一起并將其放入另一個(gè)變量。

因?yàn)槟拇a太多,只是為了更改變量,所以組織這些變量的方式將對(duì)代碼的編寫方式以及維護(hù)代碼的方式產(chǎn)生很大的影響。

一次只需考慮幾個(gè)變量即可使事情變得容易。JavaScript有一種幫助您完成此任務(wù)的方法,稱為范圍。由于作用域在JavaScript中的工作方式,因此函數(shù)無法訪問其他函數(shù)中定義的變量。

很好 這意味著在處理一個(gè)功能時(shí),您只需考慮一個(gè)功能即可。您不必?fù)?dān)心其他函數(shù)會(huì)對(duì)您的變量產(chǎn)生什么影響。

不過,它也有一個(gè)缺點(diǎn)。確實(shí)很難在不同功能之間共享變量。

如果您確實(shí)想在范圍之外共享變量怎么辦?一種常見的處理方法是將其放在您之上的范圍內(nèi),例如全局范圍內(nèi)。

您可能還記得jQuery時(shí)代的情況。在加載任何jQuery插件之前,必須確保jQuery在全局范圍內(nèi)。

這行得通,但它們是一些令人討厭的問題。

首先,所有腳本標(biāo)簽都必須以正確的順序排列。然后,您必須小心確保沒有人弄亂該命令。

如果您確實(shí)弄亂了該順序,則在運(yùn)行過程中,您的應(yīng)用程序?qū)⒁l(fā)錯(cuò)誤。當(dāng)該函數(shù)在全局范圍內(nèi)尋找期望的jQuery時(shí),如果找不到它,它將拋出錯(cuò)誤并停止執(zhí)行。

這使得維護(hù)代碼變得棘手。它使刪除舊代碼或腳本標(biāo)簽成為輪盤游戲。您不知道會(huì)發(fā)生什么。代碼的這些不同部分之間的依賴關(guān)系是隱式的。任何函數(shù)都可以捕獲全局的所有內(nèi)容,因此您不知道哪個(gè)函數(shù)取決于哪個(gè)腳本。

第二個(gè)問題是,因?yàn)檫@些變量在全局范圍內(nèi),所以該全局范圍內(nèi)的代碼的每個(gè)部分都可以更改該變量。惡意代碼可以有意更改該變量,以使您的代碼執(zhí)行您不希望這樣做的事情,或者非惡意代碼可能會(huì)無意間破壞了您的變量。

模塊如何提供幫助?

模塊為您提供了更好的方式來組織這些變量和函數(shù)。使用模塊,您可以將有意義的變量和函數(shù)組合在一起。

這會(huì)將這些函數(shù)和變量放入模塊范圍。模塊作用域可用于在模塊中的功能之間共享變量。

但是與函數(shù)作用域不同,模塊作用域具有一種使其變量也可用于其他模塊的方式。他們可以明確地說出模塊中的哪些變量,類或函數(shù)應(yīng)該可用。

當(dāng)其他模塊可以使用某些東西時(shí),這稱為導(dǎo)出。導(dǎo)出后,其他模塊可以明確地說它們依賴于該變量,類或函數(shù)。

因?yàn)檫@是一種明確的關(guān)系,所以您可以判斷如果刪除另一個(gè)模塊,哪個(gè)模塊將中斷。

一旦能夠在模塊之間導(dǎo)出和導(dǎo)入變量,就可以更輕松地將代碼分解為可以相互獨(dú)立工作的小塊。然后,您可以組合并重組這些塊(類似于Lego塊),以從同一組模塊創(chuàng)建所有不同種類的應(yīng)用程序。

由于模塊是如此有用,因此曾多次嘗試將模塊功能添加到JavaScript。今天,有兩個(gè)模塊系統(tǒng)正在積極使用中。Node.js歷史上一直使用CommonJS(CJS)。ESM(EcmaScript模塊)是一個(gè)更新的系統(tǒng),已添加到JavaScript規(guī)范中。瀏覽器已經(jīng)支持ES模塊,并且Node正在添加支持。

讓我們深入了解這個(gè)新的模塊系統(tǒng)是如何工作的。

ES模塊如何工作

當(dāng)您使用模塊進(jìn)行開發(fā)時(shí),您將建立一個(gè)依賴關(guān)系圖。不同依賴項(xiàng)之間的連接來自您使用的任何導(dǎo)入語句。

這些import語句是瀏覽器或Node確切知道其需要加載哪些代碼的方式。您給它一個(gè)文件,以用作圖形的入口點(diǎn)。從那里開始,它緊隨任何import語句以查找其余代碼。

但是文件本身不是瀏覽器可以使用的東西。它需要解析所有這些文件,以將它們轉(zhuǎn)換為稱為模塊記錄的數(shù)據(jù)結(jié)構(gòu)。這樣,它實(shí)際上知道文件中正在發(fā)生什么。

之后,需要將模塊記錄轉(zhuǎn)換為模塊實(shí)例。實(shí)例結(jié)合了兩件事:代碼和狀態(tài)。

該代碼基本上是一組指令。這就像如何做某事的食譜。但是,就其本身而言,您不能使用該代碼執(zhí)行任何操作。您需要原材料才能與這些說明一起使用。

什么是狀態(tài)?國家給你那些原材料。狀態(tài)是變量在任何時(shí)間點(diǎn)的實(shí)際值。當(dāng)然,這些變量只是內(nèi)存中保存值的框的昵稱。

因此,模塊實(shí)例將代碼(指令列表)與狀態(tài)(所有變量的值)組合在一起。

我們需要的是每個(gè)模塊的模塊實(shí)例。模塊加載的過程正在從該入口點(diǎn)文件變?yōu)榫哂型暾哪K實(shí)例圖。

對(duì)于ES模塊,此過程分為三個(gè)步驟。

  1. 構(gòu)造—查找,下載所有文件并將其解析為模塊記錄。
  2. 實(shí)例化—查找內(nèi)存中的框以放置所有導(dǎo)出的值(但尚未用值填充它們)。然后使導(dǎo)出和導(dǎo)入都指向內(nèi)存中的那些框。這稱為鏈接。
  3. 評(píng)估—運(yùn)行代碼以將變量的實(shí)際值填充到框中。

人們談?wù)揈S模塊是異步的。您可以將其視為異步的,因?yàn)楣ぷ鞣譃槿齻€(gè)不同的階段(加載,實(shí)例化和評(píng)估),并且這些階段可以分別完成。

這意味著規(guī)范確實(shí)引入了CommonJS中不存在的一種異步。我將在后面解釋,但是在CJS中,一個(gè)模塊及其下面的依賴項(xiàng)一次全部被加載,實(shí)例化和評(píng)估,而中間沒有任何中斷。

但是,這些步驟本身不一定是異步的。它們可以以同步方式完成。這取決于正在執(zhí)行的加載。這是因?yàn)椴⒎撬袃?nèi)容都由ES模塊規(guī)范控制。實(shí)際上有兩部分工作,涵蓋了不同的規(guī)格。

ES模塊規(guī)范說,你應(yīng)該如何解析文件到模塊的記錄,你應(yīng)該如何實(shí)例化和評(píng)估模塊。但是,它沒有說明如何首先獲取文件。

讀取文件的是加載程序。加載程序是在其他規(guī)范中指定的。對(duì)于瀏覽器,該規(guī)范是HTML規(guī)范。但是您可以根據(jù)所使用的平臺(tái)使用不同的裝載程序。

加載程序還可以精確控制模塊的加載方式。它調(diào)用ES模塊的方法-?ParseModule,Module.InstantiateModule.Evaluate。有點(diǎn)像操縱JS引擎的字符串的控制器。

現(xiàn)在,讓我們?cè)敿?xì)介紹每個(gè)步驟。

建造

在構(gòu)建階段,每個(gè)模塊發(fā)生三件事。

  1. 找出從哪里下載包含模塊的文件(又稱模塊分辨率)
  2. 提取文件(通過從URL下載文件或從文件系統(tǒng)加載文件)
  3. 將文件解析為模塊記錄

查找文件并獲取

加載程序?qū)⒇?fù)責(zé)查找文件并下載。首先,它需要找到入口點(diǎn)文件。在HTML中,您可以通過腳本標(biāo)記告訴加載程序在哪里找到它。

具有type = module屬性和src URL的腳本標(biāo)記。 src URL有一個(gè)文件,它是條目

但是,如何找到下一組模塊-main.js直接依賴的模塊呢?

這就是導(dǎo)入語句的來源。導(dǎo)入語句的一部分稱為模塊說明符。它告訴加載程序可以在哪里找到每個(gè)下一個(gè)模塊。

關(guān)于模塊說明符的一件事:在瀏覽器和Node之間有時(shí)需要對(duì)它們進(jìn)行不同的處理。每個(gè)主機(jī)都有自己的解釋模塊說明符字符串的方式。為此,它使用一種稱為模塊解析算法的方法,該算法在平臺(tái)之間有所不同。當(dāng)前,某些可在Node中工作的模塊說明符將無法在瀏覽器中工作,但仍在進(jìn)行修復(fù)。

在此之前,瀏覽器僅接受URL作為模塊說明符。他們將從該URL加載模塊文件。但這不會(huì)同時(shí)出現(xiàn)在整個(gè)圖形上。在解析文件之前,您不知道模塊需要獲取哪些依賴項(xiàng),并且在獲取文件之前,您無法解析該文件。

這意味著我們必須逐層遍歷樹,解析一個(gè)文件,然后找出其依賴項(xiàng),然后查找并加載這些依賴項(xiàng)。

如果主線程要等待這些文件中的每一個(gè)下載,則許多其他任務(wù)將堆積在其隊(duì)列中。

那是因?yàn)楫?dāng)您在瀏覽器中工作時(shí),下載部分會(huì)花費(fèi)很長時(shí)間。

延遲圖表顯示,如果CPU周期花費(fèi)1秒,則主內(nèi)存訪問將花費(fèi)6分鐘,而從全美的服務(wù)器中獲取文件將花費(fèi)4年

這樣阻塞主線程會(huì)使使用模塊的應(yīng)用程序使用起來太慢。這是ES模塊規(guī)范將算法分為多個(gè)階段的原因之一。將構(gòu)造分為自己的階段,使瀏覽器可以在開始實(shí)例化的同步工作之前獲取文件并增強(qiáng)對(duì)模塊圖的理解。

這種方法(將算法分為多個(gè)階段)是ES模塊和CommonJS模塊之間的主要區(qū)別之一。

CommonJS可以做不同的事情,因?yàn)閺奈募到y(tǒng)加載文件比在Internet上下載花費(fèi)的時(shí)間少得多。這意味著Node可以在加載文件時(shí)阻止主線程。并且由于文件已經(jīng)加載,因此僅實(shí)例化和求值(在CommonJS中不是獨(dú)立的階段)是有意義的。這也意味著在返回模塊實(shí)例之前,您要遍歷整棵樹,加載,實(shí)例化和評(píng)估任何依賴項(xiàng)。

CommonJS方法有一些含義,我將在后面詳細(xì)解釋。但是,這意味著一件事,就是在帶有CommonJS模塊的Node中,可以在模塊說明符中使用變量。require在尋找下一個(gè)模塊之前,您正在執(zhí)行該模塊中的所有代碼(直到語句)。這意味著當(dāng)您進(jìn)行模塊解析時(shí),變量將具有一個(gè)值。

但是,使用ES模塊時(shí),您需要在進(jìn)行任何評(píng)估之前預(yù)先建立整個(gè)模塊圖。這意味著您不能在模塊說明符中包含變量,因?yàn)檫@些變量尚無值。

但是有時(shí)將變量用于模塊路徑確實(shí)很有用。例如,您可能要根據(jù)代碼在做什么或在什么環(huán)境中運(yùn)行來切換要加載的模塊。

為了使ES模塊成為可能,有一個(gè)建議叫做動(dòng)態(tài)導(dǎo)入。借助它,您可以使用類似的導(dǎo)入語句import(${path}/foo.js)

這種工作方式是將使用加載的任何文件import()作為單獨(dú)圖形的入口點(diǎn)進(jìn)行處理。動(dòng)態(tài)導(dǎo)入的模塊將啟動(dòng)一個(gè)新圖,該圖將被單獨(dú)處理。

不過要注意一件事–這兩個(gè)圖中的任何模塊都將共享一個(gè)模塊實(shí)例。這是因?yàn)榧虞d程序會(huì)緩存模塊實(shí)例。對(duì)于特定全局范圍內(nèi)的每個(gè)模塊,將只有一個(gè)模塊實(shí)例。

這意味著發(fā)動(dòng)機(jī)的工作量更少。例如,這意味著即使多個(gè)模塊依賴于該模塊文件,該模塊文件也只會(huì)被提取一次。(這是緩存模塊的一個(gè)原因。我們將在評(píng)估部分中看到另一個(gè)原因。)

加載程序使用稱為模塊映射的內(nèi)容來管理此緩存。每個(gè)全局變量在單獨(dú)的模塊圖中跟蹤其模塊。

當(dāng)加載程序獲取URL時(shí),它將將該URL放入模塊映射中,并記下當(dāng)前正在獲取文件。然后它將發(fā)出請(qǐng)求并繼續(xù)以開始獲取下一個(gè)文件。

如果另一個(gè)模塊依賴于同一文件會(huì)怎樣?加載程序?qū)⒃谀K映射中查找每個(gè)URL。如果在其中看到fetching,它將繼續(xù)前進(jìn)到下一個(gè)URL。

但是模塊圖不僅跟蹤正在獲取的文件。模塊映射還充當(dāng)模塊的緩存,我們將在后面看到。

解析中

現(xiàn)在我們已經(jīng)獲取了該文件,我們需要將其解析為模塊記錄。這有助于瀏覽器了解模塊的不同部分。

創(chuàng)建模塊記錄后,將其放置在模塊圖中。這意味著無論何時(shí)從此處請(qǐng)求,加載程序都可以將其從該映射中拉出。

解析中有一個(gè)細(xì)節(jié)看似微不足道,但實(shí)際上有很大的含義。解析所有模塊,就像它們"use strict"位于頂部一樣。也有其他細(xì)微的差異。例如,關(guān)鍵字await是在模塊的頂級(jí)代碼中保留的,并且thisis的值undefined。

這種不同的解析方式稱為“解析目標(biāo)”。如果您解析相同的文件但使用不同的目標(biāo),那么最終將得到不同的結(jié)果。因此,您想在開始解析之前就知道要解析的文件類型-是否是模塊。

在瀏覽器中,這非常簡(jiǎn)單。您只需放入type="module"script標(biāo)簽。這告訴瀏覽器應(yīng)將此文件解析為模塊。而且由于只能導(dǎo)入模塊,因此瀏覽器知道任何導(dǎo)入也是模塊。

但是在Node中,您不使用HTML標(biāo)記,因此無法選擇使用type屬性。社區(qū)嘗試解決此問題的一種方法是使用 .mjs擴(kuò)展。使用該擴(kuò)展名告訴Node,“此文件是一個(gè)模塊”。您會(huì)看到人們將其視為解析目標(biāo)的信號(hào)。目前討論仍在進(jìn)行中,因此尚不清楚Node社區(qū)最終將決定使用什么信號(hào)。

無論哪種方式,加載程序都將確定是否將文件解析為模塊。如果它是一個(gè)模塊并且有導(dǎo)入,則它將重新開始該過程,直到提取并解析了所有文件。

我們完成了!在加載過程結(jié)束時(shí),您已經(jīng)從只有入口點(diǎn)文件變成了擁有大量模塊記錄。

下一步是實(shí)例化此模塊,并將所有實(shí)例鏈接在一起。

實(shí)例化

就像我之前提到的,實(shí)例將代碼與狀態(tài)結(jié)合在一起。該狀態(tài)存在于內(nèi)存中,因此實(shí)例化步驟就是將所有事物連接到內(nèi)存。

首先,JS引擎創(chuàng)建一個(gè)模塊環(huán)境記錄。這將管理模塊記錄的變量。然后,它會(huì)在內(nèi)存中找到所有導(dǎo)出的框。模塊環(huán)境記錄將跟蹤與每個(gè)導(dǎo)出關(guān)聯(lián)的內(nèi)存中的哪個(gè)框。

內(nèi)存中的這些框尚無法獲取其值。只有在評(píng)估之后,它們的實(shí)際值才會(huì)被填寫。該規(guī)則有一個(gè)警告:在此階段中初始化所有導(dǎo)出的函數(shù)聲明。這使評(píng)估工作變得更加容易。

為了實(shí)例化模塊圖,引擎將進(jìn)行深度優(yōu)先的后順序遍歷。這意味著它將下降到圖表的底部-底部的不依賴于其他任何東西的依賴項(xiàng)-并設(shè)置其導(dǎo)出。

引擎完成了模塊下面所有出口的接線-該模塊所依賴的所有出口。然后,它返回一個(gè)級(jí)別,以連接從該模塊導(dǎo)入的內(nèi)容。

請(qǐng)注意,導(dǎo)出和導(dǎo)入都指向內(nèi)存中的同一位置。首先連接出口,可以確保所有進(jìn)口都可以連接到匹配的出口。

這不同于CommonJS模塊。在CommonJS中,整個(gè)導(dǎo)出對(duì)象在導(dǎo)出時(shí)被復(fù)制。這意味著導(dǎo)出的任何值(如數(shù)字)都是副本。

這意味著,如果導(dǎo)出模塊以后更改了該值,則導(dǎo)入模塊將看不到該更改。

相反,ES模塊使用稱為實(shí)時(shí)綁定的東西。兩個(gè)模塊都指向內(nèi)存中的相同位置。這意味著,當(dāng)導(dǎo)出模塊更改值時(shí),該更改將顯示在導(dǎo)入模塊中。

導(dǎo)出值的模塊可以隨時(shí)更改這些值,但是導(dǎo)入模塊不能更改其導(dǎo)入的值。話雖如此,如果模塊導(dǎo)入了一個(gè)對(duì)象,則它可以更改該對(duì)象上的屬性值。

之所以擁有這樣的實(shí)時(shí)綁定,是因?yàn)槟梢栽诓贿\(yùn)行任何代碼的情況下連接所有模塊。當(dāng)您具有循環(huán)依賴性時(shí),這將有助于評(píng)估,如下所述。

因此,在此步驟結(jié)束時(shí),我們已為導(dǎo)出/導(dǎo)入的變量連接了所有實(shí)例和存儲(chǔ)位置。

現(xiàn)在我們可以開始評(píng)估代碼,并使用它們的值填充這些內(nèi)存位置。

評(píng)估

最后一步是將這些框填充到內(nèi)存中。JS引擎通過執(zhí)行頂級(jí)代碼(函數(shù)外部的代碼)來實(shí)現(xiàn)此目的。

除了僅在內(nèi)存中填充這些框外,評(píng)估代碼還可能觸發(fā)副作用。例如,一個(gè)模塊可能會(huì)調(diào)用服務(wù)器。

由于存在潛在的副作用,您只需要評(píng)估該模塊一次。與實(shí)例化中發(fā)生的鏈接可以完全相同的結(jié)果執(zhí)行多次相反,評(píng)估可以根據(jù)您執(zhí)行多少次而得出不同的結(jié)果。

這是擁有模塊映射的原因之一。模塊映射通過規(guī)范的URL緩存模塊,因此每個(gè)模塊只有一個(gè)模塊記錄。這樣可以確保每個(gè)模塊僅執(zhí)行一次。與實(shí)例化一樣,這是作為深度優(yōu)先的后遍歷來完成的。

那我們之前討論過的那些周期呢?

在循環(huán)依賴性中,您最終在圖形中產(chǎn)生了一個(gè)循環(huán)。通常,這是一個(gè)漫長的循環(huán)。但是為了解釋這個(gè)問題,我將使用一個(gè)簡(jiǎn)短的循環(huán)的人為例子。

讓我們看一下如何將其與CommonJS模塊一起使用。首先,主模塊將執(zhí)行直到require語句。然后它將去加載計(jì)數(shù)器模塊。

然后,計(jì)數(shù)器模塊將嘗試message從導(dǎo)出對(duì)象進(jìn)行訪問。但是由于尚未在主模塊中對(duì)此進(jìn)行評(píng)估,因此它將返回undefined。JS引擎將在內(nèi)存中為局部變量分配空間,并將其值設(shè)置為undefined。

評(píng)估一直持續(xù)到計(jì)數(shù)器模塊頂級(jí)代碼的末尾。我們想看看我們是否最終將獲得正確的消息值(在評(píng)估m(xù)ain.js之后),因此我們?cè)O(shè)置了超時(shí)時(shí)間。然后評(píng)估在上恢復(fù)main.js。

消息變量將被初始化并添加到內(nèi)存中。但是由于兩者之間沒有連接,因此在所需的模塊中它將保持未定義狀態(tài)。

如果使用實(shí)時(shí)綁定處理導(dǎo)出,則計(jì)數(shù)器模塊最終將看到正確的值。到超時(shí)運(yùn)行時(shí),main.js的評(píng)估就已經(jīng)完成并填寫了值。

支持這些循環(huán)是ES模塊設(shè)計(jì)背后的重要理由。正是這種三相設(shè)計(jì)使它們成為可能。

ES模塊的狀態(tài)如何?

隨著5月初Firefox 60的發(fā)布,默認(rèn)情況下,所有主流瀏覽器都將支持ES模塊。Node還增加了支持,該工作組致力于解決CommonJS和ES模塊之間的兼容性問題。

這意味著您將能夠?qū)cript標(biāo)簽與一起使用type=module,并使用導(dǎo)入和導(dǎo)出。但是,更多的模塊功能尚未出現(xiàn)。該動(dòng)態(tài)導(dǎo)入的提案是在第3階段在規(guī)范過程中,由于是import.meta這將有助于支持Node.js的使用情況,以及模塊解決方案也將有助于平滑過度瀏覽器和Node.js的差異 因此,您可以期望與模塊的合作在將來會(huì)變得更好。

參考

ES modules: A cartoon deep-dive

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容