“睡服”面试官系列第十一篇之module加载实现(建议收藏学习)
目錄
1. 瀏覽器加載
1.1傳統(tǒng)方法
1.2加載規(guī)則
2. ES6 模塊與 CommonJS 模塊的差異
3. Node 加載
3.1概述
3.2內部變量
4ES6 模塊加載 CommonJS 模塊
5CommonJS 模塊加載 ES6 模塊
6循環(huán)加載
6.1CommonJS 模塊的加載原理
6.2CommonJS 模塊的循環(huán)加載
7ES6 模塊的循環(huán)加載
8ES6 模塊的轉碼
8.1ES6 module transpiler
8.2SystemJS
總結
“睡服“面試官系列之各系列目錄匯總(建議學習收藏)
1. 瀏覽器加載
1.1傳統(tǒng)方法
HTML 網(wǎng)頁中,瀏覽器通過 <script> 標簽加載 JavaScript 腳本
<!-- 頁面內嵌的腳本 --> <script type="application/javascript"> // module code </script> <!-- 外部腳本 --> <script type="application/javascript" src="path/to/myModule.js"> </script>上面代碼中,由于瀏覽器腳本的默認語言是 JavaScript,因此 type="application/javascript" 可以省略。
默認情況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到 <script> 標簽就會停下來,等到執(zhí)行完腳本,再繼續(xù)向下渲染。如果是外部腳本,還
必須加入腳本下載的時間。
如果腳本體積很大,下載和執(zhí)行的時間就會很長,因此造成瀏覽器堵塞,用戶會感覺到瀏覽器“卡死”了,沒有任何響應。這顯然是很不好的體驗,所以瀏覽
器允許腳本異步加載,下面就是兩種異步加載的語法
上面代碼中, <script> 標簽打開 defer 或 async 屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執(zhí)行,
而是直接執(zhí)行后面的命令。
defer 與 async 的區(qū)別是: defer 要等到整個頁面在內存中正常渲染結束(DOM 結構完全生成,以及其他腳本執(zhí)行完成),才會執(zhí)行; async 一旦下載
完,渲染引擎就會中斷渲染,執(zhí)行這個腳本以后,再繼續(xù)渲染。一句話, defer 是“渲染完再執(zhí)行”, async 是“下載完就執(zhí)行”。另外,如果有多個 defer
腳本,會按照它們在頁面出現(xiàn)的順序加載,而多個 async 腳本是不能保證加載順序的。
1.2加載規(guī)則
瀏覽器加載 ES6 模塊,也使用 <script> 標簽,但是要加入 type="module" 屬性。
<script type="module" src="./foo.js"></script>上面代碼在網(wǎng)頁中插入一個模塊 foo.js ,由于 type 屬性設為 module ,所以瀏覽器知道這是一個 ES6 模塊。
瀏覽器對于帶有 type="module" 的 <script> ,都是異步加載,不會造成堵塞瀏覽器,即等到整個頁面渲染完,再執(zhí)行模塊腳本,等同于打開了 <script>
標簽的 defer 屬性
如果網(wǎng)頁有多個 <script type="module"> ,它們會按照在頁面出現(xiàn)的順序依次執(zhí)行。
<script> 標簽的 async 屬性也可以打開,這時只要加載完成,渲染引擎就會中斷渲染立即執(zhí)行。執(zhí)行完成后,再恢復渲染
一旦使用了 async 屬性, <script type="module"> 就不會按照在頁面出現(xiàn)的順序執(zhí)行,而是只要該模塊加載完成,就執(zhí)行該模塊。
ES6 模塊也允許內嵌在網(wǎng)頁中,語法行為與加載外部腳本完全一致
對于外部的模塊腳本(上例是 foo.js ),有幾點需要注意。
代碼是在模塊作用域之中運行,而不是在全局作用域運行。模塊內部的頂層變量,外部不可見。
模塊腳本自動采用嚴格模式,不管有沒有聲明 use strict 。
模塊之中,可以使用 import 命令加載其他模塊( .js 后綴不可省略,需要提供絕對 URL 或相對 URL),也可以使用 export 命令輸出對外接口。
模塊之中,頂層的 this 關鍵字返回 undefined ,而不是指向 window 。也就是說,在模塊頂層使用 this 關鍵字,是無意義的。
同一個模塊如果加載多次,將只執(zhí)行一次。
下面是一個示例模塊
利用頂層的 this 等于 undefined 這個語法點,可以偵測當前代碼是否在 ES6 模塊之中
const isNotModuleScript = this !== undefined;2. ES6 模塊與 CommonJS 模塊的差異
討論 Node 加載 ES6 模塊之前,必須了解 ES6 模塊與 CommonJS 模塊完全不同。
它們有兩個重大差異。
CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
第二個差異是因為 CommonJS 加載的是一個對象(即 module.exports 屬性),該對象只有在腳本運行完才會生成。而 ES6 模塊不是對象,它的對外接
口只是一種靜態(tài)定義,在代碼靜態(tài)解析階段就會生成。
下面重點解釋第一個差異。
CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個模塊文件 lib.js 的例子。
上面代碼輸出內部變量 counter 和改寫這個變量的內部方法 incCounter 。然后,在 main.js 里面加載這個模塊。
// main.js var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 3上面代碼說明, lib.js 模塊加載以后,它的內部變化就影響不到輸出的 mod.counter 了。這是因為 mod.counter 是一個原始類型的值,會被緩存。除非寫
成一個函數(shù),才能得到內部變動后的值。
上面代碼中,輸出的 counter 屬性實際上是一個取值器函數(shù)。現(xiàn)在再執(zhí)行 main.js ,就可以正確讀取內部變量 counter 的變動了。
$ node main.js 3 4ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態(tài)分析的時候,遇到模塊加載命令 import ,就會生成一個只讀引用。等到腳本真正執(zhí)行
時,再根據(jù)這個只讀引用,到被加載的那個模塊里面去取值。換句話說,ES6 的 import 有點像 Unix 系統(tǒng)的“符號連接”,原始值變了, import 加載的值
也會跟著變。因此,ES6 模塊是動態(tài)引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊。
還是舉上面的例子。
上面代碼說明,ES6 模塊輸入的變量 counter 是活的,完全反應其所在模塊 lib.js 內部的變化。
再舉一個出現(xiàn)在 export 一節(jié)中的例子
上面代碼中, m1.js 的變量 foo ,在剛加載時等于 bar ,過了 500 毫秒,又變?yōu)榈扔?baz 。
讓我們看看, m2.js 能否正確讀取這個變化。
上面代碼表明,ES6 模塊不會緩存運行結果,而是動態(tài)地去被加載的模塊取值,并且變量總是綁定其所在的模塊。
由于 ES6 輸入的模塊變量,只是一個“符號連接”,所以這個變量是只讀的,對它進行重新賦值會報錯。
上面代碼中, main.js 從 lib.js 輸入變量 obj ,可以對 obj 添加屬性,但是重新賦值就會報錯。因為變量 obj 指向的地址是只讀的,不能重新賦值,這就
好比 main.js 創(chuàng)造了一個名為 obj 的 const 變量。
最后, export 通過接口,輸出的是同一個值。不同的腳本加載這個接口,得到的都是同樣的實例
上面的腳本 mod.js ,輸出的是一個 C 的實例。不同的腳本加載這個模塊,得到的都是同一個實例
// x.js import {c} from './mod'; c.add(); // y.js import {c} from './mod'; c.show() // main.js import './x'; import './y';現(xiàn)在執(zhí)行 main.js ,輸出的是 1?
$ babel-node main.js 1這就證明了 x.js 和 y.js 加載的都是 C 的同一個實例
3. Node 加載
3.1概述
Node 對 ES6 模塊的處理比較麻煩,因為它有自己的 CommonJS 模塊格式,與 ES6 模塊格式是不兼容的。目前的解決方案是,將兩者分開,ES6 模塊
和 CommonJS 采用各自的加載方案。
Node 要求 ES6 模塊采用 .mjs 后綴文件名。也就是說,只要腳本文件里面使用 import 或者 export 命令,那么就必須采用 .mjs 后綴名。 require 命令不
能加載 .mjs 文件,會報錯,只有 import 命令才可以加載 .mjs 文件。反過來, .mjs 文件里面也不能使用 require 命令,必須使用 import 。
目前,這項功能還在試驗階段。安裝 Node v8.5.0 或以上版本,要用 --experimental-modules 參數(shù)才能打開該功能
為了與瀏覽器的 import 加載規(guī)則相同,Node 的 .mjs 文件支持 URL 路徑
import './foo?query=1'; // 加載 ./foo 傳入?yún)?shù) ?query=1上面代碼中,腳本路徑帶有參數(shù) ?query=1 ,Node 會按 URL 規(guī)則解讀。同一個腳本只要參數(shù)不同,就會被加載多次,并且保存成不同的緩存。由于這個
原因,只要文件名中含有 : 、 % 、 # 、 ? 等特殊字符,最好對這些字符進行轉義。
目前,Node 的 import 命令只支持加載本地模塊( file: 協(xié)議),不支持加載遠程模塊。
如果模塊名不含路徑,那么 import 命令會去 node_modules 目錄尋找這個模塊。
如果模塊名包含路徑,那么 import 命令會按照路徑去尋找這個名字的腳本文件
import 'file:///etc/config/app.json'; import './foo'; import './foo?search'; import '../bar'; import '/baz';如果腳本文件省略了后綴名,比如 import './foo' ,Node 會依次嘗試四個后綴名: ./foo.mjs 、 ./foo.js 、 ./foo.json 、 ./foo.node 。如果這些腳
本文件都不存在,Node 就會去加載 ./foo/package.json 的 main 字段指定的腳本。如果 ./foo/package.json 不存在或者沒有 main 字段,那么就會依次
加載 ./foo/index.mjs 、 ./foo/index.js 、 ./foo/index.json 、 ./foo/index.node 。如果以上四個文件還是都不存在,就會拋出錯誤。
最后,Node 的 import 命令是異步加載,這一點與瀏覽器的處理方法相同
3.2內部變量
ES6 模塊應該是通用的,同一個模塊不用修改,就可以用在瀏覽器環(huán)境和服務器環(huán)境。為了達到這個目標,Node 規(guī)定 ES6 模塊之中不能使用
CommonJS 模塊的特有的一些內部變量。
首先,就是 this 關鍵字。ES6 模塊之中,頂層的 this 指向 undefined ;CommonJS 模塊的頂層 this 指向當前模塊,這是兩者的一個重大差異。
其次,以下這些頂層變量在 ES6 模塊之中都是不存在的
arguments
require
module
exports
__filename
__dirname
如果你一定要使用這些變量,有一個變通方法,就是寫一個 CommonJS 模塊輸出這些變量,然后再用 ES6 模塊加載這個 CommonJS 模塊。但是這樣
一來,該 ES6 模塊就不能直接用于瀏覽器環(huán)境了,所以不推薦這樣做。
上面代碼中, expose.js 是一個 CommonJS 模塊,輸出變量 __dirname ,該變量在 ES6 模塊之中不存在。ES6 模塊加載 expose.js ,就可以得到
__dirname 。
4ES6 模塊加載 CommonJS 模塊
CommonJS 模塊的輸出都定義在 module.exports 這個屬性上面。Node 的 import 命令加載 CommonJS 模塊,Node 會自動將 module.exports 屬
性,當作模塊的默認輸出,即等同于 export default xxx 。
下面是一個 CommonJS 模塊。
import 命令加載上面的模塊, module.exports 會被視為默認輸出,即 import 命令實際上輸入的是這樣一個對象 { default: module.exports } 。
所以,一共有三種寫法,可以拿到 CommonJS 模塊的 module.exports 。
上面代碼的第三種寫法,可以通過 baz.default 拿到 module.exports 。 foo 屬性和 bar 屬性就是可以通過這種方法拿到了 module.exports 。
下面是一些例子
上面代碼中, es.js 采用第二種寫法時,要通過 bar.default 這樣的寫法,才能拿到 module.exports?
// c.js module.exports = function two() { return 2; }; // es.js import foo from './c'; foo(); // 2 import * as bar from './c'; bar.default(); // 2 bar(); // throws, bar is not a function上面代碼中, bar 本身是一個對象,不能當作函數(shù)調用,只能通過 bar.default 調用。
CommonJS 模塊的輸出緩存機制,在 ES6 加載方式下依然有效
上面代碼中,對于加載 foo.js 的腳本, module.exports 將一直是 123 ,而不會變成 null 。
由于 ES6 模塊是編譯時確定輸出接口,CommonJS 模塊是運行時確定輸出接口,所以采用 import 命令加載 CommonJS 模塊時,不允許采用下面的寫
法。
上面的寫法不正確,因為 fs 是 CommonJS 格式,只有在運行時才能確定 readfile 接口,而 import 命令要求編譯時就確定這個接口。解決方法就是改
為整體輸入。
5CommonJS 模塊加載 ES6 模塊
CommonJS 模塊加載 ES6 模塊,不能使用 require 命令,而要使用 import() 函數(shù)。ES6 模塊的所有輸出接口,會成為輸入對象的屬性
// es.mjs let foo = { bar: 'my-default' }; export default foo; foo = null; // cjs.js const es_namespace = await import('./es'); // es_namespace = { // get default() { // ... // } // } console.log(es_namespace.default); // { bar:'my-default' }上面代碼中, default 接口變成了 es_namespace.default 屬性。另外,由于存在緩存機制, es.js 對 foo 的重新賦值沒有在模塊外部反映出來。
下面是另一個例子。
6循環(huán)加載
“循環(huán)加載”(circular dependency)指的是, a 腳本的執(zhí)行依賴 b 腳本,而 b 腳本的執(zhí)行又依賴 a 腳本。
// a.js var b = require('b'); // b.js var a = require('a');通常,“循環(huán)加載”表示存在強耦合,如果處理不好,還可能導致遞歸加載,使得程序無法執(zhí)行,因此應該避免出現(xiàn)。
但是實際上,這是很難避免的,尤其是依賴關系復雜的大項目,很容易出現(xiàn) a 依賴 b , b 依賴 c , c 又依賴 a 這樣的情況。這意味著,模塊加載機制必須考
慮“循環(huán)加載”的情況。
對于 JavaScript 語言來說,目前最常見的兩種模塊格式 CommonJS 和 ES6,處理“循環(huán)加載”的方法是不一樣的,返回的結果也不一樣。
6.1CommonJS 模塊的加載原理
介紹 ES6 如何處理“循環(huán)加載”之前,先介紹目前最流行的 CommonJS 模塊格式的加載原理。
CommonJS 的一個模塊,就是一個腳本文件。 require 命令第一次加載該腳本,就會執(zhí)行整個腳本,然后在內存生成一個對象
上面代碼就是 Node 內部加載模塊后生成的一個對象。該對象的 id 屬性是模塊名, exports 屬性是模塊輸出的各個接口, loaded 屬性是一個布爾值,表
示該模塊的腳本是否執(zhí)行完畢。其他還有很多屬性,這里都省略了。
以后需要用到這個模塊的時候,就會到 exports 屬性上面取值。即使再次執(zhí)行 require 命令,也不會再次執(zhí)行該模塊,而是到緩存之中取值。也就是說,
CommonJS 模塊無論加載多少次,都只會在第一次加載時運行一次,以后再加載,就返回第一次運行的結果,除非手動清除系統(tǒng)緩存。
6.2CommonJS 模塊的循環(huán)加載
CommonJS 模塊的重要特性是加載時執(zhí)行,即腳本代碼在 require 的時候,就會全部執(zhí)行。一旦出現(xiàn)某個模塊被"循環(huán)加載",就只輸出已經(jīng)執(zhí)行的部
分,還未執(zhí)行的部分不會輸出。
讓我們來看,Node 官方文檔里面的例子。腳本文件 a.js 代碼如下。
上面代碼之中, a.js 腳本先輸出一個 done 變量,然后加載另一個腳本文件 b.js 。注意,此時 a.js 代碼就停在這里,等待 b.js 執(zhí)行完畢,再往下執(zhí)行。
再看 b.js 的代碼。
上面代碼之中, b.js 執(zhí)行到第二行,就會去加載 a.js ,這時,就發(fā)生了“循環(huán)加載”。系統(tǒng)會去 a.js 模塊對應對象的 exports 屬性取值,可是因為 a.js
還沒有執(zhí)行完,從 exports 屬性只能取回已經(jīng)執(zhí)行的部分,而不是最后的值。
a.js 已經(jīng)執(zhí)行的部分,只有一行。
因此,對于 b.js 來說,它從 a.js 只輸入一個變量 done ,值為 false 。
然后, b.js 接著往下執(zhí)行,等到全部執(zhí)行完畢,再把執(zhí)行權交還給 a.js 。于是, a.js 接著往下執(zhí)行,直到執(zhí)行完畢。我們寫一個腳本 main.js ,驗證這
個過程。
執(zhí)行 main.js ,運行結果如下。
$ node main.js 在 b.js 之中,a.done = false b.js 執(zhí)行完畢 在 a.js 之中,b.done = true a.js 執(zhí)行完畢 在 main.js 之中, a.done=true, b.done=true上面的代碼證明了兩件事。一是,在 b.js 之中, a.js 沒有執(zhí)行完畢,只執(zhí)行了第一行。二是, main.js 執(zhí)行到第二行時,不會再次執(zhí)行 b.js ,而是輸出
緩存的 b.js 的執(zhí)行結果,即它的第四行
總之,CommonJS 輸入的是被輸出值的拷貝,不是引用。
另外,由于 CommonJS 模塊遇到循環(huán)加載時,返回的是當前已經(jīng)執(zhí)行的部分的值,而不是代碼全部執(zhí)行后的值,兩者可能會有差異。所以,輸入變量的
時候,必須非常小心。
上面代碼中,如果發(fā)生循環(huán)加載, require('a').foo 的值很可能后面會被改寫,改用 require('a') 會更保險一點
7ES6 模塊的循環(huán)加載
ES6 處理“循環(huán)加載”與 CommonJS 有本質的不同。ES6 模塊是動態(tài)引用,如果使用 import 從一個模塊加載變量(即 import foo from 'foo' ),那些
變量不會被緩存,而是成為一個指向被加載模塊的引用,需要開發(fā)者自己保證,真正取值的時候能夠取到值。
請看下面這個例子。
上面代碼中, a.mjs 加載 b.mjs , b.mjs 又加載 a.mjs ,構成循環(huán)加載。執(zhí)行 a.mjs ,結果如下
$ node --experimental-modules a.mjs b.mjs ReferenceError: foo is not defined上面代碼中,執(zhí)行 a.mjs 以后會報錯, foo 變量未定義,這是為什么?
讓我們一行行來看,ES6 循環(huán)加載是怎么處理的。首先,執(zhí)行 a.mjs 以后,引擎發(fā)現(xiàn)它加載了 b.mjs ,因此會優(yōu)先執(zhí)行 b.mjs ,然后再執(zhí)行 a.js 。接
著,執(zhí)行 b.mjs 的時候,已知它從 a.mjs 輸入了 foo 接口,這時不會去執(zhí)行 a.mjs ,而是認為這個接口已經(jīng)存在了,繼續(xù)往下執(zhí)行。執(zhí)行到第三行
console.log(foo) 的時候,才發(fā)現(xiàn)這個接口根本沒定義,因此報錯。
解決這個問題的方法,就是讓 b.mjs 運行的時候, foo 已經(jīng)有定義了。這可以通過將 foo 寫成函數(shù)來解決。
這時再執(zhí)行 a.mjs 就可以得到預期結果
$ node --experimental-modules a.mjs b.mjs foo a.mjs bar這是因為函數(shù)具有提升作用,在執(zhí)行 import {bar} from './b' 時,函數(shù) foo 就已經(jīng)有定義了,所以 b.mjs 加載的時候不會報錯。這也意味著,如果把函
數(shù) foo 改寫成函數(shù)表達式,也會報錯
上面代碼的第四行,改成了函數(shù)表達式,就不具有提升作用,執(zhí)行就會報錯。
我們再來看 ES6 模塊加載器SystemJS給出的一個例子
上面代碼中, even.js 里面的函數(shù) even 有一個參數(shù) n ,只要不等于 0,就會減去 1,傳入加載的 odd() 。 odd.js 也會做類似操作。
運行上面這段代碼,結果如下
上面代碼中,參數(shù) n 從 10 變?yōu)?0 的過程中, even() 一共會執(zhí)行 6 次,所以變量 counter 等于 6。第二次調用 even() 時,參數(shù) n 從 20 變?yōu)?0,
even() 一共會執(zhí)行 11 次,加上前面的 6 次,所以變量 counter 等于 17。
這個例子要是改寫成 CommonJS,就根本無法執(zhí)行,會報錯
上面代碼中, even.js 加載 odd.js ,而 odd.js 又去加載 even.js ,形成“循環(huán)加載”。這時,執(zhí)行引擎就會輸出 even.js 已經(jīng)執(zhí)行的部分(不存在任何結
果),所以在 odd.js 之中,變量 even 等于 null ,等到后面調用 even(n-1) 就會報錯
8ES6 模塊的轉碼
瀏覽器目前還不支持 ES6 模塊,為了現(xiàn)在就能使用,可以將轉為 ES5 的寫法。除了 Babel 可以用來轉碼之外,還有以下兩個方法,也可以用來轉碼
8.1ES6 module transpiler
ES6 module transpiler是 square 公司開源的一個轉碼器,可以將 ES6 模塊轉為 CommonJS 模塊或 AMD 模塊的寫法,從而在瀏覽器中使用。
首先,安裝這個轉碼器
然后,使用 compile-modules convert 命令,將 ES6 模塊文件轉碼。
$ compile-modules convert file1.js file2.js-o 參數(shù)可以指定轉碼后的文件名。
$ compile-modules convert -o out.js file1.js8.2SystemJS
另一種解決方法是使用 SystemJS。它是一個墊片庫(polyfill),可以在瀏覽器內加載 ES6 模塊、AMD 模塊和 CommonJS 模塊,將其轉為 ES5 格
式。它在后臺調用的是 Google 的 Traceur 轉碼器。
使用時,先在網(wǎng)頁內載入 system.js 文件。
然后,使用 System.import 方法加載模塊文件
<script> System.import('./app.js'); </script>上面代碼中的 ./app ,指的是當前目錄下的 app.js 文件。它可以是 ES6 模塊文件, System.import 會自動將其轉碼。
需要注意的是, System.import 使用異步加載,返回一個 Promise 對象,可以針對這個對象編程。下面是一個模塊文件
然后,在網(wǎng)頁內加載這個模塊文件
<script> System.import('app/es6-file').then(function(m) { console.log(new m.q().es6); // hello }); </script>上面代碼中, System.import 方法返回的是一個 Promise 對象,所以可以用 then 方法指定回調函數(shù)。
總結
本博客源于本人閱讀相關書籍和視頻總結,創(chuàng)作不易,謝謝點贊支持。學到就是賺到。我是歌謠,勵志成為一名優(yōu)秀的技術革新人員。
歡迎私信交流,一起學習,一起成長。
推薦鏈接 其他文件目錄參照
“睡服“面試官系列之各系列目錄匯總(建議學習收藏)
總結
以上是生活随笔為你收集整理的“睡服”面试官系列第十一篇之module加载实现(建议收藏学习)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 计算机操作系统的图示,电脑操作系统位数的
- 下一篇: 博客搬家工具