ES6標(biāo)準(zhǔn)入門 摘要 (Module的加載實(shí)現(xiàn))

瀏覽器加載

傳統(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è)對象編程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容