無模塊化
簡單的將所有的 js 文件統(tǒng)統(tǒng)放在一起麻掸,然后通過 <script>
標簽引入酥夭。
- 優(yōu)點:
- 相比于使用一個js文件,這種多個js文件實現(xiàn)最簡單的模塊化的思想是進步的脊奋。
- 缺點:
- 污染全局作用域熬北。因為每一個模塊都是暴露在全局的,簡單的使用诚隙,會導(dǎo)致全局變量命名沖突讶隐,當(dāng)然,我們也可以使用命名空間的方式來解決久又。
- 對于大型項目巫延,各種js很多,開發(fā)人員必須手動解決模塊和代碼庫的依賴關(guān)系地消,后期維護成本較高炉峰。
- 依賴關(guān)系不明顯,不利于維護脉执。 比如 main.js 需要使用 jquery疼阔,但是,從上面的文件中适瓦,我們是看不出來的竿开,如果 jquery 忘記了,那么就會報錯玻熙。
<!-- 頁面內(nèi)嵌的腳本 -->
<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í)行的時間就會很長盯质,因此造成瀏覽器堵塞袁串,用戶會感覺到瀏覽器“卡死”了,沒有任何響應(yīng)呼巷。 - 瀏覽器允許腳本異步加載囱修,下面就是兩種異步加載的語法。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
上面代碼中王悍,
<script>
標簽打開 defer 或 async 屬性破镰,腳本就會異步加載。渲染引擎遇到這一行命令压储,就會開始下載外部腳本鲜漩,但不會等它下載和執(zhí)行,而是直接執(zhí)行后面的命令渠脉。
- defer 與 async 的區(qū)別是:
- defer 要等到整個頁面在內(nèi)存中正常渲染結(jié)束(DOM 結(jié)構(gòu)完全生成宇整,以及其他腳本執(zhí)行完成),才會執(zhí)行
- async 一旦下載完芋膘,渲染引擎就會中斷渲染鳞青,執(zhí)行這個腳本以后,再繼續(xù)渲染
- 一句話为朋,defer 是 “渲染完再執(zhí)行”臂拓,async 是 “下載完就執(zhí)行”
- 如果有多個 defer 腳本,會按照它們在頁面出現(xiàn)的順序加載习寸,而多個 async 腳本是不能保證加載順序的
CommonJS 規(guī)范
1. 概述
- 是一個 JavaScript 模塊化的規(guī)范胶惰,
Nodejs
環(huán)境所使用的模塊系統(tǒng)就是基于CommonJS
規(guī)范實現(xiàn)的,我們現(xiàn)在所說的CommonJS
規(guī)范也大多是指 Node 的模塊系統(tǒng)霞溪,前端的 webpack 也是對 CommonJS 原生支持孵滞。 - CommonJS 規(guī)范中捆,每一個文件就是一個模塊,其內(nèi)部定義的變量是屬于這個模塊的坊饶,不會對外暴露泄伪,也就是說不會污染全局變量。
- 有四個重要的環(huán)境變量為模塊化的實現(xiàn)提供支持:
module
匿级、exports
蟋滴、require
、global
痘绎。實際使用時津函,用module.exports
定義當(dāng)前模塊對外輸出的接口(不推薦直接用 exports ),用require
加載模塊孤页。
// example.js
var x = 5;
var addX = function (value) {
return value + x;
};
上面代碼中尔苦,變量 x 和函數(shù) addX,是當(dāng)前文件 example.js 私有的散庶,其他文件不可見蕉堰。
- 如果想在多個文件分享變量,必須定義為
global
對象的屬性悲龟。
global.warning = true
上面代碼的 warning 變量屋讶,可以被所有文件讀取。當(dāng)然须教,這樣寫法是不推薦的皿渗。
- CommonJS 規(guī)范規(guī)定,每個模塊內(nèi)部轻腺,module 變量代表當(dāng)前模塊乐疆。這個變量是一個對象,它的 exports 屬性(即module.exports)是對外的接口贬养。加載某個模塊挤土,其實是加載該模塊的 module.exports 屬性。
// example.js
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports = {
x: x,
addX: addX
};
上面代碼通過 module.exports 輸出變量 x 和函數(shù) addX误算。
-
require
方法用于加載模塊
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
-
CommonJS 規(guī)范特點
- 所有代碼都運行在模塊作用域仰美,不會污染全局作用域
- CommonJS 模塊可以多次加載,但是只會在第一次加載時運行一次儿礼,然后運行結(jié)果就被緩存了咖杂,以后再加載,就直接讀取緩存結(jié)果蚊夫。要想讓模塊再次運行诉字,必須清除緩存
- CommonJS 模塊加載的順序,按照其在代碼中出現(xiàn)的順序
- 由于 Node.js 主要用于服務(wù)器編程,模塊文件一般都已經(jīng)存在于本地硬盤壤圃,所以加載起來比較快陵霉,不用考慮非同步加載的方式,所以 CommonJS 規(guī)范比較適用
-
優(yōu)點:
- CommonJS 規(guī)范在服務(wù)器端率先完成了 JavaScript 的模塊化伍绳,解決了依賴撩匕、全局變量污染的問題,這也是 js 運行在服務(wù)器端的必要條件墨叛。
-
缺點:
- 由于 CommonJS 是同步加載模塊的,在服務(wù)器端模蜡,文件都是保存在硬盤上漠趁,所以同步加載沒有問題,但是對于瀏覽器端忍疾,需要將文件從服務(wù)器端請求過來闯传,那么同步加載就不適用了,所以卤妒,CommonJS 是不適用于瀏覽器端的甥绿。
2. module 對象
- Node 內(nèi)部提供一個 Module 構(gòu)建函數(shù)。所有模塊都是 Module 的實例则披。
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
// ...
- 每個模塊內(nèi)部共缕,都有一個 module 對象,代表當(dāng)前模塊士复。它有以下屬性图谷。
module.id 模塊的識別符,通常是帶有絕對路徑的模塊文件名阱洪。
module.filename 模塊的文件名便贵,帶有絕對路徑。
module.loaded 返回一個布爾值冗荸,表示模塊是否已經(jīng)完成加載承璃。
module.parent 返回一個對象,表示調(diào)用該模塊的模塊蚌本。
module.children 返回一個數(shù)組盔粹,表示該模塊要用到的其他模塊。
module.exports 表示模塊對外輸出的值魂毁。
- 如果在命令行下調(diào)用某個模塊玻佩,比如 node something.js ,那么 module.parent 就是 null 席楚。如果是在腳本之中調(diào)用咬崔,比如 require('./something.js') ,那么 module.parent 就是調(diào)用它的模塊。利用這一點垮斯,可以判斷當(dāng)前模塊是否為入口腳本郎仆。
if (!module.parent) {
// ran with `node something.js`
app.listen(8088, function() {
console.log('app listening on port 8088');
});
} else {
// used with `require('/.something.js')`
module.exports = app;
}
- module.exports 屬性表示當(dāng)前模塊對外輸出的接口,其他文件加載該模塊兜蠕,實際上就是讀取 module.exports 變量扰肌。
var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();
setTimeout(function() {
module.exports.emit('ready');
}, 1000);
上面模塊會在加載后1秒后,發(fā)出 ready 事件熊杨。其他文件監(jiān)聽該事件曙旭,可以寫成下面這樣。
var a = require('./a');
a.on('ready', function() {
console.log('module a is ready');
});
3. exports 變量
- 為了方便晶府,Node 為每個模塊提供一個 exports 變量桂躏,指向 module.exports。這等同在每個模塊頭部川陆,有一行這樣的命令剂习。
var exports = module.exports;
造成的結(jié)果是,在對外輸出模塊接口時较沪,可以向exports對象添加方法鳞绕。
exports.area = function (r) {
return Math.PI * r * r;
};
exports.circumference = function (r) {
return 2 * Math.PI * r;
};
- 注意,不能直接將 exports 變量指向一個值尸曼,因為這樣等于切斷了 exports 與 module.exports 的聯(lián)系们何。
exports.hello = function() {
return 'hello';
};
module.exports = 'Hello world';
上面代碼中,hello 函數(shù)是無法對外輸出的控轿,因為 module.exports 被重新賦值了
- 這意味著垂蜗,如果一個模塊的對外接口,就是一個單一的值解幽,不能使用 exports 輸出贴见,只能使用 module.exports 輸出,如下。
module.exports = function (x) { console.log(x); };
4. require 命令
4.1 基本用法
Node 使用 CommonJS 模塊規(guī)范躲株,內(nèi)置的 require 命令用于加載模塊文件
- require 命令的基本功能是片部,讀入并執(zhí)行一個 JavaScript 文件,然后返回該模塊的 exports 對象霜定。如果沒有發(fā)現(xiàn)指定模塊档悠,會報錯。
// example.js
var invisible = function () {
console.log("invisible");
}
exports.message = "hi";
exports.say = function () {
console.log(message);
}
運行以下命令輸出 exports 對象
var example = require('./example.js');
example
// {
// message: "hi",
// say: [Function]
// }
- 如果模塊輸出的是一個函數(shù)望浩,那就不能定義在 exports 對象上面辖所,而要定義在 module.exports 變量上面。
module.exports = function () {
console.log("hello world");
};
require('./example2.js')();
4.2 加載規(guī)則
- require 命令用于加載文件磨德,后綴名默認為 .js缘回。
var foo = require('foo');
// 等同于
var foo = require('foo.js');
- 根據(jù)參數(shù)的不同格式吆视,require命令去不同路徑尋找模塊文件。
- (1)如果參數(shù)字符串以
/
開頭酥宴,則表示加載的是一個位于絕對路徑的模塊文件啦吧。比如,require('/home/marco/foo.js') 將加載 /home/marco/foo.js拙寡。 - (2)如果參數(shù)字符串以
./
開頭授滓,則表示加載的是一個位于相對路徑(跟當(dāng)前執(zhí)行腳本的位置相比)的模塊文件。比如肆糕,require('./circle') 將加載當(dāng)前腳本同一目錄的 circle.js般堆。 - (3)如果參數(shù)字符串不以
./
或/
開頭,則表示加載的是一個默認提供的核心模塊(位于 Node 的系統(tǒng)安裝目錄中
)诚啃,或者一個位于各級 node_modules 目錄的已安裝模塊
(全局安裝或局部安裝)郁妈。
- (1)如果參數(shù)字符串以
- 舉例來說,腳本 /home/user/projects/foo.js 執(zhí)行了 require('bar.js') 命令绍申,Node 會依次搜索以下文件。
- (4)如果參數(shù)字符串不以
./
或/
開頭顾彰,而且是一個路徑极阅,比如 require('example-module/path/to/file'),則將先找到 example-module 的位置涨享,然后再以它為參數(shù)筋搏,找到后續(xù)路徑。 - (5)如果指定的模塊文件沒有發(fā)現(xiàn)厕隧,Node 會嘗試為文件名添加 .js 奔脐、 .json 、 .node 后吁讨,再去搜索髓迎。 .js 件會以文本格式的 JavaScript 腳本文件解析, .json 文件會以 JSON 格式的文本文件解析建丧, .node 文件會以編譯后的二進制文件解析排龄。
- (6)如果想得到 require 命令加載的確切文件名,使用 require.resolve() 方法翎朱。
- (4)如果參數(shù)字符串不以
/usr/local/lib/node/bar.js
/home/user/projects/node_modules/bar.js
/home/user/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
這樣設(shè)計的目的是橄维,使得不同的模塊可以將所依賴的模塊本地化
4.3 目錄的加載規(guī)則
- 通常,我們會把相關(guān)的文件會放在一個目錄里面拴曲,便于組織争舞。這時,最好為該目錄設(shè)置一個入口文件澈灼,讓 require 方法可以通過這個入口文件竞川,加載整個目錄。
在目錄中放置一個 package.json 文件,并且將入口文件寫入 main 字段流译。下面是一個例子逞怨。
// package.json
{
"name" : "some-library",
"main" : "./lib/some-library.js"
}
- require 發(fā)現(xiàn)參數(shù)字符串指向一個目錄以后,會自動查看該目錄的 package.json 文件福澡,然后加載 main 字段指定的入口文件叠赦。如果 package.json 文件沒有 main 字段,或者根本就沒有 package.json 文件革砸,則會加載該目錄下的 index.js 文件或 index.node 文件除秀。
4.4 模塊的緩存
- 第一次加載某個模塊時,Node 會緩存該模塊算利。以后再加載該模塊册踩,就直接從緩存取出該模塊的 module.exports 屬性。
require('./example.js');
require('./example.js').message = "hello";
require('./example.js').message
// "hello"
上面代碼中效拭,連續(xù)三次使用 require 命令暂吉,加載同一個模塊。第二次加載的時候缎患,為輸出的對象添加了一個 message 屬性慕的。但是第三次加載的時候,這個 message 屬性依然存在挤渔,這就證明 require 命令并沒有重新加載模塊文件肮街,而是輸出了緩存。
- 如果想要多次執(zhí)行某個模塊判导,可以讓該模塊輸出一個函數(shù)嫉父,然后每次 require 這個模塊的時候,重新執(zhí)行一下輸出的函數(shù)眼刃。
所有緩存的模塊保存在 require.cache 之中绕辖,如果想刪除模塊的緩存,可以像下面這樣寫擂红。
// 刪除指定模塊的緩存
delete require.cache[moduleName];
// 刪除所有模塊的緩存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
});
- 注意引镊,緩存是根據(jù)絕對路徑識別模塊的,如果同樣的模塊名篮条,但是保存在不同的路徑对碌,require 命令還是會重新加載該模塊俱笛。
5. 環(huán)境變量 NODE_PATH
- Node 執(zhí)行一個腳本時玛痊,會先查看環(huán)境變量 NODE_PATH商玫。它是一組以冒號分隔的絕對路徑。在其他位置找不到指定模塊時伴栓,Node 會去這些路徑查找伦连。
可以將 NODE_PATH 添加到 .bashrc雨饺。
export NODE_PATH="/usr/local/lib/node"
所以,如果遇到復(fù)雜的相對路徑惑淳,比如下面這樣额港。
var myModule = require('../../../../lib/myModule');
- 有兩種解決方法,一是將該文件加入 node_modules 目錄歧焦,二是修改 NODE_PATH 環(huán)境變量移斩,package.json 文件可以采用下面的寫法。
{
"name": "node_path",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "NODE_PATH=lib node index.js"
},
"author": "",
"license": "ISC"
}
- NODE_PATH 是歷史遺留下來的一個路徑解決方案绢馍,通常不應(yīng)該使用向瓷,而應(yīng)該使用 node_modules 目錄機制。
4.6 模塊的循環(huán)加載
- 如果發(fā)生模塊的循環(huán)加載舰涌,即A加載B猖任,B又加載A,則B將加載A的不完整版本瓷耙。
// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';
// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
上面代碼是三個 JavaScript 文件朱躺。其中,a.js 加載了 b.js搁痛,而 b.js 又加載 a.js长搀。這時, Node 返回 a.js 的不完整版本落追,所以執(zhí)行結(jié)果如下。
$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
修改 main.js涯肩,再次加載 a.js 和 b.js轿钠。
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
執(zhí)行上面代碼
$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
main.js a2
main.js b2
- 上面代碼中,第二次加載 a.js 和 b.js 時病苗,會直接從緩存讀取 exports 屬性疗垛,所以 a.js 和 b.js 內(nèi)部的 console.log 語句都不會執(zhí)行了。
4.7 require.main
- require 方法有一個 main 屬性硫朦,可以用來判斷模塊是直接執(zhí)行贷腕,還是被調(diào)用執(zhí)行。
直接執(zhí)行的時候(node module.js)咬展,require.main 屬性指向模塊本身泽裳。
require.main === module
// true
調(diào)用執(zhí)行的時候(通過 require 加載該腳本執(zhí)行),上面的表達式返回 false 破婆。
5. 模塊的加載機制
- CommonJS 模塊輸出的是一個值的拷貝涮总,一旦輸出一個值,模塊內(nèi)部的變化就影響不到這個值
- CommonJS 模塊是運行時加載祷舀,加載的是一個對象(即
module.exports
屬性)瀑梗,該對象只有在腳本運行完才會生成
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
上面代碼輸出內(nèi)部變量 counter 和改寫這個變量的內(nèi)部方法 incCounter烹笔。然后,在 main.js 里面加載這個模塊抛丽。
// main.js
var com = require('./lib')
console.log(com.counter) // 3
com.incCounter()
console.log(com.counter) // 3
上面代碼說明谤职,
lib.js
模塊加載以后,它的內(nèi)部變化就影響不到輸出的com.counter
了亿鲜。這是因為com.counter
是一個原始類型的值允蜈,會被緩存。除非寫成一個函數(shù)狡门,才能得到內(nèi)部變動后的值陷寝。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
上面代碼中,輸出的 counter 屬性實際上是一個取值器函數(shù)∑淞螅現(xiàn)在再執(zhí)行 main.js 凤跑,就可以正確讀取內(nèi)部變量 counter 的變動了。
$ node main.js
3
4
5.1 require 的內(nèi)部處理流程
- require 命令是 CommonJS 規(guī)范之中叛复,用來加載其他模塊的命令仔引。它其實不是一個全局命令,而是指向當(dāng)前模塊的 module.require 命令褐奥,而后者又調(diào)用 Node 的內(nèi)部命令 Module._load 咖耘。
Module._load = function(request, parent, isMain) {
// 1. 檢查 Module._cache,是否緩存之中有指定模塊
// 2. 如果緩存之中沒有撬码,就創(chuàng)建一個新的Module實例
// 3. 將它保存到緩存
// 4. 使用 module.load() 加載指定的模塊文件儿倒,
// 讀取文件內(nèi)容之后,使用 module.compile() 執(zhí)行文件代碼
// 5. 如果加載/解析過程報錯呜笑,就從緩存刪除該模塊
// 6. 返回該模塊的 module.exports
};
上面的第 4 步夫否,采用 module.compile() 執(zhí)行指定模塊的腳本,邏輯如下叫胁。
Module.prototype._compile = function(content, filename) {
// 1. 生成一個require函數(shù)凰慈,指向module.require
// 2. 加載其他輔助方法到require
// 3. 將文件內(nèi)容放到一個函數(shù)之中,該函數(shù)可調(diào)用 require
// 4. 執(zhí)行該函數(shù)
};
上面的第 1 步和第 2 步驼鹅, require 函數(shù)及其輔助方法主要如下微谓。
require(): 加載外部模塊
require.resolve():將模塊名解析到一個絕對路徑
require.main:指向主模塊
require.cache:指向所有緩存的模塊
require.extensions:根據(jù)文件的后綴名,調(diào)用不同的執(zhí)行函數(shù)
一旦 require 函數(shù)準備完畢输钩,整個所要加載的腳本內(nèi)容豺型,就被放到一個新的函數(shù)之中,這樣可以避免污染全局環(huán)境买乃。該函數(shù)的參數(shù)包括 require触创、module、exports 为牍,以及其他一些參數(shù)哼绑。
(function (exports, require, module, __filename, __dirname) {
// YOUR CODE INJECTED HERE!
});
- Module._compile 方法是同步執(zhí)行的岩馍,所以 Module._load 要等它執(zhí)行完成,才會向用戶返回 module.exports 的值
CommonJS 規(guī)范 require 方法簡單實現(xiàn)
- 通過讀取文件內(nèi)容將內(nèi)容包裝到一個自執(zhí)行函數(shù)中抖韩,默認返回 module.exports 做為函數(shù)的結(jié)果蛀恩。
const a = `function (exports, require, module, __filename, __dirname) {
let a = 1;
module.exports = 'hello';
return module.exports;
}(exports, require, module, xxxx, xxx)`;
function Module(id) {
this.id = id;
// 代表的是模塊的返回結(jié)果
this.exports = {};
}
Module._cache = {};
Module.wrapper = [
`(function(exports, require, module, __filename, __dirname) {`,
`})`
];
Module._extensions = {
'.js'(module) {
let content = fs.readFileSync(module.id, 'utf8');
content = Module.wrapper[0] + content + Module.wrapper[1];
// 需要讓函數(shù)字符串變成真正的函數(shù)
let fn = vm.runInThisContext(content);
let exports = module.exports; // {}
let dirname = path.dirname(module.id);
// 讓包裝的函數(shù)執(zhí)行 require 時會讓包裝的函數(shù)執(zhí)行,并且把this改變
fn.call(exports, exports, req, module, module.id, dirname);
},
'.json'(module) {
let content = fs.readFileSync(module.id, 'utf8');
module.exports = JSON.parse(content);
}
};
Module._resolveFilename = function (filename) {
let absPath = path.resolve(__dirname, filename);
// 查看路徑是否存在 如果不存在 則增加 .js 或者 .json 后綴
let isExists = fs.existsSync(absPath);
if (isExists) {
return absPath;
} else {
let keys = Object.keys(Module._extensions);
for (let i = 0; i < keys.length; i++) {
let newPath = absPath + keys[i];
let flag = fs.existsSync(newPath);
if (flag) {
return newPath;
}
}
throw new Error('module not exists');
}
};
Module.prototype.load = function () {
let extname = path.extname(this.id);
// module.exports = 'hello'
Module._extensions[extname](this);
}
function req(filename) { // 默認傳入的文件名可能沒有增加后綴茂浮,如果沒有后綴我就嘗試增加.js .json
// 解析出絕對路徑
filename = Module._resolveFilename(filename);
// 創(chuàng)建一個模塊
// 這里加載前先看一眼 是否加載過了
let cacheModule = Module._cache[filename]; // 多次引用同一個模塊只運行一次
if (cacheModule) {
return cacheModule.exports; // 返回緩存的結(jié)果即可
}
let module = new Module(filename);
Module._cache[filename] = module
// 加載模塊
module.load();
return module.exports;
};
ES6 加載規(guī)則
- 瀏覽器加載 ES6 模塊双谆,也使用
<script>
標簽,但是要加入type="module"
屬性席揽。
<script type="module" src="./foo.js"></script>
- 上面代碼在網(wǎng)頁中插入一個模塊 foo.js 顽馋,由于 type 屬性設(shè)為 module ,所以瀏覽器知道這是一個 ES6 模塊幌羞。
- 瀏覽器對于帶有 type="module" 的
<script>
寸谜,都是異步加載,不會造成堵塞瀏覽器属桦,即等到整個頁面渲染完熊痴,再執(zhí)行模塊腳本,等同于打開了<script>
標簽的 defer 屬性聂宾。
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
- 如果網(wǎng)頁有多個
<script type="module">
果善,它們會按照在頁面出現(xiàn)的順序依次執(zhí)行。 -
<script>
標簽的 async 屬性也可以打開系谐,這時只要加載完成巾陕,渲染引擎就會中斷渲染立即執(zhí)行。執(zhí)行完成后纪他,再恢復(fù)渲染鄙煤。
<script type="module" src="./foo.js" async></script>
- 一旦使用了 async 屬性,
<script type="module">
就不會按照在頁面出現(xiàn)的順序執(zhí)行止喷,而是只要該模塊加載完成馆类,就執(zhí)行該模塊混聊。 - ES6 模塊也允許內(nèi)嵌在網(wǎng)頁中弹谁,語法行為與加載外部腳本完全一致。
<script type="module">
import utils from "./utils.js";
// other code
</script>
對于外部的模塊腳本(上例是 foo.js )句喜,有幾點需要注意预愤。
- 代碼是在模塊作用域之中運行,而不是在全局作用域運行咳胃。模塊內(nèi)部的頂層變量植康,外部不可見。
- 模塊腳本自動采用嚴格模式展懈,不管有沒有聲明 use strict 销睁。
- 模塊之中供璧,可以使用 import 命令加載其他模塊( .js 后綴不可省略,需要提供絕對 URL 或相對 URL)冻记,也可以使用 export 命令輸出對外接口睡毒。
- 模塊之中,頂層的 this 關(guān)鍵字返回 undefined 冗栗,而不是指向 window 演顾。也就是說,在模塊頂層使用 this 關(guān)鍵字隅居,是無意義的钠至。
- 同一個模塊如果加載多次,將只執(zhí)行一次胎源。
示例
import utils from 'https://example.com/js/utils.js';
const x = 1;
console.log(x === window.x); //false
console.log(this === undefined); // true
利用頂層的 this 等于 undefined 這個語法點棉钧,可以偵測當(dāng)前代碼是否在 ES6 模塊之中。
const isNotModuleScript = this !== undefined;
ES6 模塊與 CommonJS 模塊的差異
- ES6 模塊的運行機制與 CommonJS 不一樣乒融。JS 引擎對腳本靜態(tài)分析的時候掰盘,遇到模塊加載命令import,就會生成一個只讀引用赞季。等到腳本真正執(zhí)行時愧捕,再根據(jù)這個只讀引用,到被加載的那個模塊里面去取值申钩。換句話說次绘,ES6 的 import 有點像 Unix 系統(tǒng)的“符號連接”,原始值變了撒遣,import 加載的值也會跟著變邮偎。因此,ES6 模塊是動態(tài)引用义黎,并且不會緩存值禾进,模塊里面的變量綁定其所在的模塊。
以之前 2-CommonJS 規(guī)范中的例子為例
// 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);