瀏覽器加載
傳統(tǒng)加載
默認(rèn)情況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到<script>標(biāo)簽就會停下來,等到執(zhí)行完腳本,再繼續(xù)向下渲染。如果是外部腳本,還必須加入腳本下載的時(shí)間。
如果腳本體積很大,下載和執(zhí)行的時(shí)間就會很長,所以瀏覽器允許腳本異步加載,下面就是兩種異步加載的語法。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
defer是“渲染完再執(zhí)行”,等到整個(gè)頁面在內(nèi)存中正常渲染結(jié)束(DOM 結(jié)構(gòu)完全生成,以及其他腳本執(zhí)行完成)
async是“下載完就執(zhí)行”,一旦下載完,渲染引擎就會中斷渲染,執(zhí)行這個(gè)腳本以后,再繼續(xù)渲染。
另外,如果有多個(gè)defer腳本,會按照它們在頁面出現(xiàn)的順序加載,而多個(gè)async腳本是不能保證加載順序的。
加載規(guī)則
瀏覽器加載 ES6 模塊,也使用<script>標(biāo)簽,但是要加入type="module"屬性,這樣瀏覽器就知道這是一個(gè)ES6模塊。
<script type="module" src="./foo.js"></script>
瀏覽器對于帶有type="module"的<script>,都是異步加載,不會造成堵塞瀏覽器,即等到整個(gè)頁面渲染完,再執(zhí)行模塊腳本,等同于打開了<script>標(biāo)簽的defer屬性。與defer一樣,如果有多個(gè),也會安裝順序加載。
也可以在引入ES6模塊的時(shí)候打開<script>標(biāo)簽的async屬性,這樣就與傳統(tǒng)加載的async屬性一樣加載完成就執(zhí)行,且不管順序。
<script type="module" src="./foo.js" async></script>
ES6 模塊也允許內(nèi)嵌在網(wǎng)頁中,語法行為與加載外部腳本完全一致。
<script type="module">
import utils from "./utils.js";
// other code
</script>
這樣寫有幾點(diǎn)需要注意
- 代碼是在模塊作用域之中運(yùn)行,而不是在全局作用域運(yùn)行。模塊內(nèi)部的頂層變量,外部不可見。
- 模塊腳本自動采用嚴(yán)格模式,不管有沒有聲明use strict
- 模塊之中,可以使用import命令加載其他模塊(.js后綴不可省略,需要提供絕對 URL 或相對 URL),也可以使用export命令輸出對外接口
- 模塊之中,頂層的this關(guān)鍵字返回undefined,而不是指向window。也就是說,在模塊頂層使用this關(guān)鍵字,是無意義的。
- 同一個(gè)模塊如果加載多次,將只執(zhí)行一次
利用頂層的this等于undefined這個(gè)語法點(diǎn),可以偵測當(dāng)前代碼是否在 ES6 模塊之中,全等于undefined則是在ES6模塊中。
ES6 模塊與 CommonJS 模塊的差異
CommonJS 模塊輸出的是一個(gè)值的拷貝,ES6 模塊輸出的是值的引用
CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個(gè)值,模塊內(nèi)部的變化就影響不到這個(gè)值。
ES6 模塊的運(yùn)行機(jī)制與 CommonJS 不一樣。JS 引擎對腳本靜態(tài)分析的時(shí)候,遇到模塊加載命令import,就會生成一個(gè)只讀引用。等到腳本真正執(zhí)行時(shí),再根據(jù)這個(gè)只讀引用,到被加載的那個(gè)模塊里面去取值。
CommonJS 模塊是運(yùn)行時(shí)加載,ES6 模塊是編譯時(shí)輸出
因?yàn)?CommonJS 加載的是一個(gè)對象(即module.exports屬性),該對象只有在腳本運(yùn)行完才會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態(tài)定義,在代碼靜態(tài)解析階段就會生成。ES6 模塊不會緩存運(yùn)行結(jié)果,而是動態(tài)地去被加載的模塊取值,并且變量總是綁定其所在的模塊。
export通過接口,輸出的是同一個(gè)值。不同的腳本加載這個(gè)接口,得到的都是同樣的實(shí)例。
// mod.js
function C() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}
export let c = new C();
// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y';
// 執(zhí)行main.js 輸出1
Node 加載
Node 要求 ES6 模塊采用.mjs后綴文件名。也就是說,只要腳本文件里面使用import或者export命令,那么就必須采用.mjs后綴名。require命令不能加載.mjs文件,會報(bào)錯(cuò)。
目前,這項(xiàng)功能還在試驗(yàn)階段。安裝 Node v8.5.0 或以上版本,要用--experimental-modules參數(shù)才能打開該功能。
$ node --experimental-modules my-app.mjs
為了與瀏覽器的import加載規(guī)則相同,Node 的.mjs文件支持 URL 路徑。
import './foo?query=1'; // 加載 ./foo 傳入?yún)?shù) ?query=1
Node 的import命令只支持加載本地模塊(file:協(xié)議),不支持加載遠(yuǎn)程模塊。
如果模塊名不含路徑,那么import命令會去node_modules目錄尋找這個(gè)模塊。
比如import './foo',Node 會依次嘗試四個(gè)后綴名:./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。如果以上四個(gè)文件還是都不存在,就會拋出錯(cuò)誤。
內(nèi)部變量
ES6 模塊應(yīng)該是通用的,同一個(gè)模塊不用修改,就可以用在瀏覽器環(huán)境和服務(wù)器環(huán)境。為了達(dá)到這個(gè)目標(biāo),Node 規(guī)定 ES6 模塊之中不能使用 CommonJS 模塊的特有的一些內(nèi)部變量。
以下這些頂層變量在 ES6 模塊之中都是不存在的
- 頂層的this指向undefined;CommonJS 模塊的頂層this指向當(dāng)前模塊
- arguments
- require
- module
- exports
- __filename
- __dirname
如果你一定要使用這些變量,有一個(gè)變通方法,就是寫一個(gè) CommonJS 模塊輸出這些變量,然后再用 ES6 模塊加載這個(gè) CommonJS 模塊。但是這樣一來,該 ES6 模塊就不能直接用于瀏覽器環(huán)境了,所以不推薦這樣做。
ES6 模塊加載 CommonJS 模塊
CommonJS 模塊的輸出都定義在module.exports這個(gè)屬性上面。Node 的import命令加載 CommonJS 模塊,Node 會自動將module.exports屬性,當(dāng)作模塊的默認(rèn)輸出,即等同于export default xxx。
// c.js
module.exports = function two() {
return 2;
};
// 等同于
export default 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
CommonJS 模塊的輸出緩存機(jī)制,在 ES6 加載方式下依然有效。
// foo.js
module.exports = 123;
setTimeout(()=> module.exports = null);
// 對于加載foo.js的腳本,module.exports將一直是123,而不會變成null。
由于 ES6 模塊是編譯時(shí)確定輸出接口,CommonJS 模塊是運(yùn)行時(shí)確定輸出接口,所以采用import命令加載 CommonJS 模塊時(shí),只能使用整體引入,不能按需引入。
// 不正確
import { readFile } from 'fs';
// 正確
import fs from 'fs';
const readFile = fs.readFile;
因?yàn)閒s是 CommonJS 格式,只有在運(yùn)行時(shí)才能確定readFile接口,而import命令要求編譯時(shí)就確定這個(gè)接口。解決方法就是改為整體輸入。
CommonJS 模塊加載 ES6 模塊
CommonJS 模塊加載 ES6 模塊,不能使用require命令,而要使用import()函數(shù)。ES6 模塊的所有輸出接口,會成為輸入對象的屬性。
// es.mjs
let foo = { bar: 'my-default' };
export default foo;
// cjs.js
const es_namespace = await import('./es.mjs');
// default接口變成了es_namespace.default屬性
console.log(es_namespace.default);
// { bar:'my-default' }
另一個(gè)例子
// es.js
export let foo = { bar:'my-default' };
export { foo as bar };
export function f() {};
export class c {};
// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
// get foo() {return foo;}
// get bar() {return foo;}
// get f() {return f;}
// get c() {return c;}
// }
循環(huán)加載
“循環(huán)加載”(circular dependency)指的是,a腳本的執(zhí)行依賴b腳本,而b腳本的執(zhí)行又依賴a腳本。
通常,“循環(huán)加載”表示存在強(qiáng)耦合,如果處理不好,還可能導(dǎo)致遞歸加載,使得程序無法執(zhí)行,因此應(yīng)該避免出現(xiàn)。
但是實(shí)際上,這是很難避免的,尤其是依賴關(guān)系復(fù)雜的大項(xiàng)目,很容易出現(xiàn)a依賴b,b依賴c,c又依賴a這樣的情況。這意味著,模塊加載機(jī)制必須考慮“循環(huán)加載”的情況。
CommonJS 模塊的循環(huán)加載
CommonJS 的一個(gè)模塊,就是一個(gè)腳本文件。require命令第一次加載該腳本,就會執(zhí)行整個(gè)腳本,然后在內(nèi)存生成一個(gè)對象。
{
id: '...',
exports: { ... },
loaded: true,
...
}
該對象的id屬性是模塊名,exports屬性是模塊輸出的各個(gè)接口,loaded屬性是一個(gè)布爾值,表示該模塊的腳本是否執(zhí)行完畢。其他還有很多屬性,這里都省略了。
以后需要用到這個(gè)模塊的時(shí)候,就會到exports屬性上面取值。即使再次執(zhí)行require命令,也不會再次執(zhí)行該模塊,而是到緩存之中取值。也就是說,CommonJS 模塊無論加載多少次,都只會在第一次加載時(shí)運(yùn)行一次,以后再加載,就返回第一次運(yùn)行的結(jié)果,除非手動清除系統(tǒng)緩存。
CommonJS 模塊的重要特性是加載時(shí)執(zhí)行,即腳本代碼在require的時(shí)候,就會全部執(zhí)行。一旦出現(xiàn)某個(gè)模塊被"循環(huán)加載",就只輸出已經(jīng)執(zhí)行的部分,還未執(zhí)行的部分不會輸出。
// a.js
exports.done = false;
var b = require('./b.js'); // 1.執(zhí)行到這就去加載執(zhí)行b.js了
console.log('在 a.js 之中,b.done = %j', b.done); // 4. b執(zhí)行完時(shí),b.done是true
exports.done = true;
console.log('a.js 執(zhí)行完畢');
// b.js
exports.done = false;
var a = require('./a.js'); // 2. 此時(shí)a只執(zhí)行了一半, 所以 a.done是false
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執(zhí)行完畢'); //3. b執(zhí)行完就把執(zhí)行權(quán)交還給a
// main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
// 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
由于 CommonJS 模塊遇到循環(huán)加載時(shí),返回的是當(dāng)前已經(jīng)執(zhí)行的部分的值,而不是代碼全部執(zhí)行后的值,兩者可能會有差異。所以,輸入變量的時(shí)候,必須非常小心。
var a = require('a'); // 安全的寫法
var foo = require('a').foo; // 危險(xiǎn)的寫法 因?yàn)閍.foo很可能在后面被改寫
exports.good = function (arg) {
return a.foo('good', arg); // 使用的是 a.foo 的最新值
};
exports.bad = function (arg) {
return foo('bad', arg); // 使用的是一個(gè)部分加載時(shí)的值
};
ES6 模塊的循環(huán)加載
ES6 處理“循環(huán)加載”與 CommonJS 有本質(zhì)的不同。ES6 模塊是動態(tài)引用,如果使用import從一個(gè)模塊加載變量(即import foo from 'foo'),那些變量不會被緩存,而是成為一個(gè)指向被加載模塊的引用,需要開發(fā)者自己保證,真正取值的時(shí)候能夠取到值。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
// nocd a.js
// b.mjs
// ReferenceError: foo is not defined
執(zhí)行a.mjs以后,引擎發(fā)現(xiàn)它加載了b.mjs,因此會優(yōu)先執(zhí)行b.mjs, 然后再執(zhí)行a.mjs。接著,執(zhí)行b.mjs的時(shí)候,已知它從a.mjs輸入了foo接口,這時(shí)不會去執(zhí)行a.mjs,而是認(rèn)為這個(gè)接口已經(jīng)存在了,繼續(xù)往下執(zhí)行。 執(zhí)行到第三行console.log(foo)的時(shí)候,才發(fā)現(xiàn)這個(gè)接口根本沒定義,因此報(bào)錯(cuò)。
解決這個(gè)問題的方法,就是讓b.mjs運(yùn)行的時(shí)候,foo已經(jīng)有定義了。這可以通過將foo寫成函數(shù)來解決。這是因?yàn)楹瘮?shù)具有提升作用,寫成函數(shù)表達(dá)式,也會報(bào)錯(cuò)。
ES6 模塊的轉(zhuǎn)碼
瀏覽器目前還不支持 ES6 模塊,為了現(xiàn)在就能使用,可以將其轉(zhuǎn)為 ES5 的寫法。除了 Babel 可以用來轉(zhuǎn)碼之外,還有以下兩個(gè)方法,也可以用來轉(zhuǎn)碼。
ES6 module transpiler
可以將 ES6 模塊轉(zhuǎn)為 CommonJS 模塊或 AMD 模塊的寫法,從而在瀏覽器中使用。
安裝:npm install -g es6-module-transpiler
使用compile-modules convert命令,將 ES6 模塊文件轉(zhuǎn)碼: compile-modules convert file1.js file2.js
-o參數(shù)可以指定轉(zhuǎn)碼后的文件名: compile-modules convert -o out.js file1.js
SystemJS
它是一個(gè)墊片庫(polyfill),可以在瀏覽器內(nèi)加載 ES6 模塊、AMD 模塊和 CommonJS 模塊,將其轉(zhuǎn)為 ES5 格式。它在后臺調(diào)用的是 Google 的 Traceur 轉(zhuǎn)碼器。
先在網(wǎng)頁內(nèi)載入system.js文件。
<script src="system.js"></script>
然后,使用System.import方法加載模塊文件。
<script>
System.import('./app.js');
</script>
System.import使用異步加載,返回一個(gè) Promise 對象,可以針對這個(gè)對象編程