傳統script標籤的代碼加載容易致使全局做用域污染,並且要維繫一系列script的書寫順序,項目一大,維護起來愈來愈困難。模塊系統經過聲明式的暴露和引用模塊使得各個模塊之間的依賴變得明顯。javascript
這部分推薦去看es-modules-a-cartoon-deep-dive,原文裏有圖,如下的內容是我的理解整理。html
分三步:java
構造階段要作三件事情:node
不一樣平臺根據本身平臺的模塊解析算法(Module Resolution Algorithm)解釋模塊指示符,瀏覽器端目前只接受url作爲指示符。不過瀏覽器未來會一樣支持內置模塊好比kv-storage。git
模塊指示符裏不能有變量可是node中commonJS是能夠有的,由於在commonJS的模塊代碼裏,require
聲明前的代碼是會先執行的,es module是最後一步再去執行,這一步才知道各個變量的具體值是多少。因此能夠在node中有以下寫法:es6
require(`${path}/sum.js`);
複製代碼
不過es module裏有另外一種寫法動態引入import()
能夠支持在代碼執行時動態引入模塊,能夠在指示符裏攜帶變量github
import(`${path}/sum.js`);
複製代碼
瀏覽器解析常規js文件時會解析完後再執行。和模塊的解析策略不同,這裏要告訴瀏覽器解析的是個模塊。在html中:算法
<script type="module"> import {sum} from "./sum.js" </script>
複製代碼
ps: 在node中由於沒有瀏覽器這種相似打tag的形式,有種方案是模塊文件是.mjs
後綴結尾的方案,不過目前還沒有敲定。瀏覽器
解析模塊文件爲模塊記錄,找到依賴的模塊再去下載模塊而後解析成模塊記錄,直到全部的模塊都解析成模塊記錄爲止。模塊記錄會存在當前全局的一個模塊映射裏(Module Map),能夠理解成一個緩存,下次再有相同url的模塊請求就直接從模塊映射裏拿出模塊記錄便可。緩存
將上面獲得的模塊記錄類實例化。 首先在內存中指定位置給各個模塊的export
導出的變量或者函數,接着將模塊中對應的import
部分一樣指向對應的export
的內存地址。 舉個🌰
// main.js
import {obj} from "./obj.js"
// obj.js
const obj = {a: 123};
export {obj}
複製代碼
obj.js
文件裏導出的obj
和main.js
文件裏引用的obj
是指向同一個內存地址的,這中方法就是動態綁定(live binding)。
<script type='module'> import {obj} from "./obj.js" console.log(obj); //{a: 123} setTimeout(() => { console.log(obj) //{b: 233} }, 2000); </script>
複製代碼
let obj = {
a: 123
};
setTimeout(() => {
obj = { b: 233 };
}, 1000);
export { obj };
複製代碼
下面咱們看下node中一樣的代碼的效果。
// test1.js
var obj = require("./test2.js");
console.dir(obj); // {a: 123}
setTimeout(() => {
console.dir(obj); // {a: 123}
}, 2000);
// test2.js
let obj = { a: 123 };
setTimeout(() => {
obj = { b: 233 };
}, 1000);
module.exports = obj;
複製代碼
在commonJS中require
一個對象是在內存中複製一份導出模塊的對象。動態綁定主要解決的問題就是循環引用的問題,循環引用在下面的執行階段進行解釋。 注意: es module中能夠在模塊導出的部分更改導出值如上面代碼所示,可是不能在引入部分更改。
import {obj} from "./sum.js"
obj = '233' // Uncaught TypeError: Assignment to constant variable.
複製代碼
如上報錯會提示不能給常量賦值,不過若是是對象的話能夠更改內部的key,因爲動態綁定的緣由,導出部分也會發生改變
// main.js
import {obj} from "./obj.js"
setTimeout(() => {
obj.a = '嘻嘻'
}, 1000);
// obj.js
let obj = { a: 123 };
console.log(obj); // {a: 123}
setTimeout(() => {
console.log(obj); // {a: "嘻嘻"}
}, 2000);
export { obj };
複製代碼
原文中是evaluate,我這裏理解成了執行,若有不對歡迎指出。引擎開始執行模塊了,每一個模塊只會被執行一次。在上面提到過的module map裏的模塊記錄裏會存有當前模塊的狀態是實例化中仍是實例完成仍是執行完成等。能夠避免同一個模塊文件被屢次執行。
以下在node中,兩個模塊互相引用。
// test1.js
var b = require("./test2").b;
console.dir("test1: " + b); // 'test1: test2' 🥈
var a = "test1";
exports.a = a;
// test2.js
var a = require("./test1").a;
console.log("test2: " + a); // test2: undefined 🥇
var b = "test2";
setTimeout(() => {
console.log("test2: " + a); // test2: undefined 🥉
}, 1000);
exports.b = b;
node test1.js // 啓動
複製代碼
ps: emoji裏表示打印順序 node執行某個模塊時會將當前模塊的代碼放入函數中,向這個函數傳遞module
, module.exports
, __dirname
等參數。初始的module
就是一個空對象。 test1.js執行遇到require('./test2)
時會進入test2模塊開始執行,這個時候又碰到引用test1模塊的東西;由於test1模塊沒有執行完成,它的module.exports
仍是空對象,因此這個時候test2裏的a
是undefined
。由於commonJS不是動態綁定的,so等到test1模塊執行完a
變量裏仍是undefined
es module
// es1
import { b } from "./es2.js";
console.log("es1: " + b); // es1: es2 🥈
var a = "es1";
export { a };
// es2
import { a } from "./es1.js";
console.log("es2: " + a); // es2: undefined 🥇
var b = "es2";
setTimeout(() => {
console.log("es2: " + a); // es2: es1 🥉
}, 1000);
export { b };
複製代碼
以上代碼入口是es1文件。根據打印順序來看先是執行的es2模塊,以後es1裏的a
填充了實際值,因爲是動態綁定es2中的a
中的值也在以後能取到值了。