官术网_书友最值得收藏!

4.1 模塊化的需求推演

本節(jié)將為你介紹JavaScript最初面對模塊化訴求時的解決方案及其存在的問題。

4.1.1 script標簽

在ES Module模塊化標準出現以前,JavaScript本身并沒有提供任何模塊化規(guī)范,當我們需要在項目中添加多個依賴時,往往是通過大量的由上到下并列排布的<script>標簽來實現的,很容易在舊代碼中看到類似下面這樣的代碼片段:

<link rel="stylesheet" href="./lib/bootstrap.min.css">
<link rel="stylesheet" href="./lib/bootstrap-theme.min.css">
<link rel="stylesheet" href="./lib/jQuery-table.min.css">
<link rel="stylesheet" href="./lib/flat-ui.min.css">
...
<script src="./lib/js/jQuery.min.js">
<script src="./lib/js/underscore.min.js">
<script src="./lib/js/bootstrap.min.js">
<script src="./lib/js/jQuery-table.min.js">
<script src="./lib/js/echarts.min.js">
<script src="./lib/js/angularjs.min.js">

十年前非常流行的jQuery和Bootstrap都擁有極好的插件生態(tài),許多現成的第三方庫都可以直接拿來使用,開發(fā)者普遍使用上面的方式來引用多個腳本文件。即使是在現代開發(fā)中,許多非專業(yè)的前端開發(fā)者也仍然非常喜歡使用這種方式來引入外部文件,因為這樣即使不學習前端構建工具和各種腳手架工具,也很快就可以讓自己編寫的腳本在瀏覽器中運行,而且效果還不錯。隨著項目中的代碼日漸增多,這種原始的依賴管理方案的弊端就會逐漸顯現出來。如果<script>標簽上沒有設置任何延遲執(zhí)行的屬性(defer或async屬性),那么<script>標簽的執(zhí)行就會阻塞文檔對象模型(Document Object Model,即DOM對象)的解析,加載的腳本文件越多,頁面完成初始化的時間就會越長,所以我們經常會看到<script>標簽被放在<body>標簽之后,這可讓網站首屏的內容信息先完成解析渲染,再為頁面增加交互,因為交互和邏輯能力的增加對用戶而言在視覺上幾乎是無感知的。

盡管多個<script>標簽看似將不同的代碼塊隔離到了不同的文件中,但這層代碼就像窗戶紙一樣一捅就破,每一個由<script>標簽引入的腳本文件實際上都是直接暴露在同一個全局作用域之下的,這就意味著如果參與合作的開發(fā)者在自己的腳本代碼中使用了其他某個文件使用過的標識符,那么只有最后一個被引入的腳本中的定義會生效,而先引入的腳本中的定義全都會被覆蓋掉,由此引發(fā)的混亂可想而知。在現代化基于構建的前端工程體系中,應用程序的入口已經轉移到了JavaScript中,多模塊加載順序和并發(fā)請求限制數的問題也將通過JavaScript基礎工具來實現。

4.1.2 代碼隔離

為了滿足代碼隔離的基本需求,業(yè)內出現了以立即執(zhí)行函數(Immediately Invoked Function Expression,IIFE)為模塊包裝的第一代模塊化解決方案,它的基本代碼結構如下:

;(function(window, undefined){
    //...具體的業(yè)務邏輯代碼
})(window);

在ES6標準之前,JavaScript只能使用函數來劃分作用域,也就是說JavaScript需要借助函數來解決多人協作時的代碼隔離問題。上面的代碼結構看似簡單,卻包含了非常多的基礎知識點,下面就來詳細說明。

1. 開頭的分號

在代碼段的開頭添加分號,是早期的代碼合并工具引發(fā)的。瀏覽器在加載網站資源時,同一個域名下的并發(fā)連接數是有上限的(一般為6個),例如,你的網站引用了7個外部資源,那么前6個資源會先行下載,等到其中一個完成下載后,第7個文件才會開始下載。為了提升加載性能,早期的合并工具會將多個腳本文件合并壓縮并生成一個文件,但此時定義當前模塊的function語句就會與前一個模塊結尾的語句連在一起被解析,這就會引發(fā)錯誤。合并后的腳本文件往往都是經過變量替換的,開發(fā)者也很難在生成的文件中手動解決這些錯誤。而在自執(zhí)行函數的開頭添加一個分號,就能有效避免這種問題。

2. 立即執(zhí)行函數

上述代碼的主體是一段立即執(zhí)行函數,也就是我們常說的IIFE,小括號將function(){}定義語句括起來,這個括號的作用是將函數定義變成一個表達式(當然這并不是唯一的方法),緊接其后的括號里的是函數調用語句,這個匿名函數會在定義后直接運行。這樣,函數體中使用的標識符就都只在當前函數作用域有效了,立即執(zhí)行函數就是通過這種方式來達到代碼隔離效果的。

3. 函數的形參和實參

許多開發(fā)者最初會被這個寫法中的兩個“window”搞得暈頭轉向,實際上只要分清楚形參和實參,就比較容易區(qū)分它們了。在代碼中創(chuàng)建一個函數時,寫在參數列表里的參數稱為“形式參數(簡稱形參)”,它代表你調用這個函數時所傳入的實際參數,無論傳入的那個實際參數的真實名稱是什么,在當前定義的函數體范圍內都可以用形參的名稱代表它。稍微改動一下上面的例子,就更容易看清楚了:

;(function(global, undefined){
    //...具體的業(yè)務邏輯代碼
})(window);

改動之后,在函數體范圍內,“global”這個標識符就代表了傳入的“window”參數,即真實的全局對象,如果你在實參處傳入Math對象,那么函數體范圍內的“global”就代表了Math,它只在自執(zhí)行函數封閉作用域中有意義。

4. undefined

我們知道“undefined”在JavaScript語言中是一個關鍵字,不僅如此,它還是全局對象的一個屬性,它的值被定義為“undefined”。在低版本的瀏覽器中,它是可以被賦值修改的,一旦有人惡意修改了“undefined”這個屬性的值,那么你寫在代碼里的所有針對“undefined”的判斷邏輯就會混亂。由于立即執(zhí)行函數中的最后一個形參沒有對應于任何值,因此其會被自動賦值為真正的“undefined”,以避免上述風險。另一方面,“undefined”作為形參時,一些代碼壓縮工具也會對其進行有效的壓縮和變量替換,從而減小文件體積,所以在第三方工具庫的腳本文件中,我們經常會看到這種書寫風格。在JavaScript中可以使用“void 0”來得到真實的“undefined”。

5. 與外部作用域的通信

如果我們將所有的模塊代碼都編寫在自執(zhí)行函數中,那么函數執(zhí)行結束后,這些模塊代碼就會被銷毀,其中的某些執(zhí)行結果或定義的方法又該如何傳達給外界呢?常見的方法有以下兩種。第一,函數實參為對象類型時,函數體內只保留對原對象的引用,對實參執(zhí)行的所有操作都會直接影響到原對象。這就好比是在上面的模型中,我們在函數體內定義了一些方法,然后把它掛載在“window”對象的某個命名空間下,這時我們所掛載的目標對象實際上是外層“window”對象的引用,所以在函數執(zhí)行完畢后,它對“window”對象的影響也會保留下來,因為銷毀的只是對它的一個引用,就好像你在系統中刪除了一個快捷方式的圖標一樣。第二,在形式上更貼近模塊化規(guī)范,自執(zhí)行函數也是一個函數,它是可以有返回值的,我們可以把自執(zhí)行函數內部定義的方法通過“return”語句返回,然后將其賦值給另一個變量,這樣函數內部的值或方法就可以傳遞到函數外部了。需要注意的是,在IIFE函數體中書寫的對于global變量的賦值并不會影響外部的全局對象,它只會讓global這個本地變量指向堆內存中的另一個地址,只有當你對global變量的某個屬性進行賦值操作時,相應的值才會出現在全局對象上,這也是初學者非常容易忽略的知識。

4.1.3 依賴管理

借助于前文中介紹的模塊化方案,我們能夠在一定程度上解決代碼隔離的問題,然而,當完整的代碼被劃分為模塊以后,我們又需要對模塊的加載順序和相互之間的依賴關系進行管理。這件事情乍看起來似乎并沒有那么重要,在項目依賴較少時,我們可以通過手動排序來避免沖突,隨后每一次增加外部依賴,幾乎都是按次序繼續(xù)寫在已有的<script>標簽之下,那么為什么要對依賴關系圖進行解析管理呢?

首先,需要明確的是,盡管HTML標準為<script>標簽的async和defer這兩個異步加載的屬性使得加載腳本時可以不阻塞主線程,但瀏覽器在實現上并不是完全遵循標準的,每個瀏覽器在實現層面都會以自己的方式對加載和執(zhí)行的過程進行優(yōu)化。在真實的使用場景中,基于瀏覽器的不同和網絡條件的差異,<script>標簽的異步屬性對腳本加載順序的影響是不穩(wěn)定的,這就讓開發(fā)者陷入了一個兩難的境地,同步加載的話會導致頁面的等待時間越來越長,異步加載的話依賴關系又會無法保障。常規(guī)的腳本在加載完成后就會自動執(zhí)行,如果訪問的模塊還沒有解析就會引發(fā)錯誤。如果不同模塊的依賴關系非常明確,我們就可以在代碼層面對這種依賴關系進行強制加載,并對執(zhí)行順序進行限制,這樣做能夠盡量避免環(huán)境差異帶來的影響,提高代碼的健壯性和穩(wěn)定性,同時清晰的依賴關系也是代碼優(yōu)化所需要的重要信息。

當然,成熟的模塊化工具還會添加許多工程化的特性,例如,在測試模式下自動為請求增加時間戳,為請求打上自定義LogID等。模塊化最基本的訴求是解決代碼隔離和依賴管理兩大問題,4.2節(jié)將具體介紹各種JavaScript模塊化規(guī)范。

主站蜘蛛池模板: 山阳县| 凤城市| 龙口市| 青州市| 措勤县| 多伦县| 徐闻县| 龙岩市| 金昌市| 阿拉善盟| 宝鸡市| 徐汇区| 青铜峡市| 鹤山市| 田林县| 永仁县| 神池县| 汨罗市| 白银市| 霍城县| 怀宁县| 珲春市| 将乐县| 通化市| 关岭| 固镇县| 武邑县| 延长县| 漳州市| 盐山县| 巫溪县| 土默特右旗| 巴林左旗| 西乡县| 江山市| 阳西县| 容城县| 泗水县| 奇台县| 阜阳市| 崇州市|