天天看點

Module加載的詳細說明-保證你有所收獲

子產品
HTML 網頁中,浏覽器通過<script>标簽加載 JavaScript 腳本。
<!-- 頁面内嵌的腳本 -->
<script type="application/javascript">
  // module code
</script>

<!-- 外部腳本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>
上面代碼中由于浏覽器腳本的預設語言是 JavaScript。
是以type="application/javascript"可以省略。      
浏覽器同步加載 JavaScript 腳本可能會産生的問題
預設情況下,浏覽器是同步加載 JavaScript 腳本.
即渲染引擎遇到<script>标簽就會停下來,等JavaScript腳本執行完後,再繼續向下渲染。
如果是外部腳本,還必須加入腳本下載下傳的時間。下載下傳完成後,在執行。
如果腳本體積很大,下載下傳和執行的時間就會很長,是以造成浏覽器堵塞。
使用者會感覺到浏覽器“卡死”了,沒有任何響應。這顯然是很不好的體驗。
然後就出現了異步加載腳本的兩種文法      
異步加載腳本的兩種文法 defer或async
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
上面代碼中,<script>标簽打開defer或async屬性,腳本就會異步加載。
渲染引擎遇到這一行指令,就會開始下載下傳外部腳本,但不會等它下載下傳和執行,而是直接執行後面的指令。      
defer 與 async的差別是
defer:要等到整個頁面在記憶體中正常渲染結束(DOM 結構完全生成,以及其他腳本執行完成),才會執行;
async:一旦下載下傳完,渲染引擎就會中斷渲染,執行這個腳本以後,再繼續渲染。
一句話,defer是“渲染完再執行”,async是“下載下傳完就執行”。
另外,如果有多個defer腳本,會按照它們在頁面出現的順序加載,而多個async腳本是不能保證加載順序的。      
浏覽器加載 ES6 子產品 type="module"
浏覽器加載 ES6 子產品,也使用<script>标簽,但是要加入type="module"屬性。
<script type="module" src="./foo.js"></script>
浏覽器就知道這是一個es6子產品。
浏覽器對于帶有type="module"的<script>,都是異步加載,不會造成堵塞浏覽器。
即等到整個頁面渲染完,再執行<script type="module" src="./foo.js"></script>子產品腳本
也就是說 type="module" 等價于 defer
如果網頁有多個<script type="module">,它們會按照在頁面出現的順序依次執行。      
子產品引入的注意點
子產品之中,可以使用import指令加載其他子產品(.js字尾不可省略,需要提供絕對 URL 或相對 URL)。
也可以使用export指令輸出對外接口。
同一個子產品如果加載多次,将隻執行一次。      
ES6 子產品與 CommonJS 子產品的差異
1.ommonJS 子產品輸出的是一個值的拷貝,輸出的是值。ES6 子產品輸出的是值的引用。
2.CommonJS 子產品是運作時加載,ES6 子產品是編譯時輸出接口。
3.CommonJS 子產品的require()是同步加載子產品。ES6 子產品的import指令是異步加載,      
詳細說下他們的第1個差異
CommonJS 子產品輸出的是值的拷貝,也就是說,一旦輸出一個值。
子產品内部的變化就影響不到這個值。請看下面這個子產品檔案
// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};      
// main.js
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter(); //調用
console.log(mod.counter); // 3      
上面代碼說明,
lib.js子產品加載以後,它的内部變化就影響不到輸出的值 mod.counter 了。
這是因為mod.counter是一個原始類型的值,會被緩存。除非寫成一個函數,才能得到内部變動後的值。      
// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

// main.js
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter(); //調用
console.log(mod.counter); // 4      
ES6 子產品的運作機制與 CommonJS 不一樣。
JS 引擎對腳本靜态分析的時候,遇到子產品加載指令import,就會生成一個隻讀引用。
等到腳本真正執行時,再根據這個隻讀引用到被加載的那個子產品裡面去取值。
換句話說,ES6 的import有點像 Unix 系統的“符号連接配接”,原始值變了,import加載的值也會跟着變。
是以,ES6 子產品是動态引用,并且不會緩存值,子產品裡面的變量綁定其所在的子產品。
還是舉上面的例子。      
// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4      
在舉一個小粒子
// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);
上面代碼中,m1.js的變量,foo,在剛加載時等于bar,過了 500 毫秒,又變為等于baz。      
Node.js 的子產品加載方法
JavaScript 現在有兩種子產品。一種是 ES6 子產品,簡稱 ESM;另一種是 CommonJS 子產品,簡稱 CJS。
CommonJS 子產品是 Node.js 專用的,與 ES6 子產品不相容。
文法上面,兩者最明顯的差異是,CommonJS 子產品使用require()和module.exports.
ES6 子產品使用import和export。

ps:從 Node.js v13.2 版本開始,Node.js 已經預設打開了 ES6 子產品支援。
Node.js 要求 ES6 子產品采用.mjs字尾檔案名。
也就是說,隻要腳本檔案裡面使用import或者export指令,那麼就必須采用.mjs字尾名。
Node.js 遇到.mjs檔案,就認為它是 ES6 子產品,預設啟用嚴格模式,不必在每個子產品檔案頂部指定"use strict"。      
友情提示
注意,ES6 子產品與 CommonJS 子產品盡量不要混用。require指令不能加載.mjs檔案,會報錯。
隻有import指令才可以加載.mjs檔案。反過來.mjs檔案裡面也不能使用require指令,必須使用import。      
簡單說一下他們的第2個差異
第二個差異是因為 CommonJS 加載的是一個對象,通過 module.exports 輸出。該對象隻有在腳本運作完才會生成。
而ES6 子產品不是對象,它的對外接口隻是一種【靜态定義】,在代碼靜态解析階段就會生成。      

遇見問題,這是你成長的機會,如果你能夠解決,這就是收獲。

繼續閱讀