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 模組
瀏覽器載入 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,
};
上面程式碼輸出內部變數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是一個原始類型的值,會被快取。除非寫成一個函數,才能得到內部變動後的值。
// 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 模組不是對象,它的對外介面只是一種【靜態定義】,在程式碼靜態解析階段就會生成。