CommonJs
,如果還沒有看,可以查詢本文章所在的專欄進行學習。CommonJs
有很多優秀的特性,下面我們再簡單的回顧一下:模組程式碼只在載入後執行;
模組只能載入一次;
模組可以請求載入其他模組;
支援迴圈依賴;
模組可以定義公共介面,其他模組可以基於這個公共介面觀察和互動;
Es Module
的獨特之處在於,既可以通過瀏覽器原生載入,也可以與第三方載入器和構建工具一起載入。Es module
模組的瀏覽器可以從頂級模組載入整個依賴圖,且是非同步完成。瀏覽器會解析入口模組,確定依賴,並行送對依賴模組的請求。這些檔案通過網路返回後,瀏覽器就會解析它們的依賴,,如果這些二級依賴還沒有載入,則會傳送更多請求。Es Module
不僅借用了 CommonJs
和 AMD
的很多優秀特性,還增加了一些新行為:Es Module
預設在嚴格模式下執行;
Es Module
不共用全域性命名空;
Es Module
頂級的 this
的值是 undefined
(常規指令碼是window
);
模組中的 var
宣告不會新增到 window
物件;
Es Module
是非同步載入和執行的;
exports
和 import
。export
命令用於規定模組的對外介面,import
命令用於輸入其他模組提供的功能。export const nickname = "moment";
export const address = "廣州";
export const age = 18;
登入後複製
const nickname = "moment";
const address = "廣州";
const age = 18;
export { nickname, address, age };
登入後複製
export function foo(x, y) {
return x + y;
}
export const obj = {
nickname: "moment",
address: "廣州",
age: 18,
};
// 也可以寫成這樣的方式
function foo(x, y) {
return x + y;
}
const obj = {
nickname: "moment",
address: "廣州",
age: 18,
};
export { foo, obj };
登入後複製
export
輸出的變數就是本來的名字,但是可以使用as
關鍵字重新命名。const address = "廣州";
const age = 18;
export { nickname as name, address as where, age as old };
登入後複製
export default "foo";
export default { name: 'moment' }
export default function foo(x,y) {
return x+y
}
export { bar, foo as default };
登入後複製
if(true){
export {...};
}
登入後複製
export
必須提供對外的介面:// 1只是一個值,不是一個介面export 1// moment只是一個值為1的變數const moment = 1export moment// function和class的輸出,也必須遵守這樣的寫法function foo(x, y) { return x+y
}export foo複製程式碼
登入後複製
export
命令定義了模組的對外介面以後,其他js檔案就可以通過 import
命令載入整個模組import {foo,age,nickname} from '模組識別符號'
登入後複製
import
命令後面接受一個花括弧,裡面指定要從其他模組匯入的變數名,而且變數名必須與被匯入模組的對外介面的名稱相同。Assignment to constant variable
的型別錯誤。import
語句中同時取得它們。可以依次列出特定的識別符號來取得,也可以使用 *
來取得:// foo.js
export default function foo(x, y) {
return x + y;
}
export const bar = 777;
export const baz = "moment";
// main.js
import { default as foo, bar, baz } from "./foo.js";
import foo, { bar, baz } from "./foo.js";
import foo, * as FOO from "./foo.js";
登入後複製
import
匯入的模組是靜態的,會使所有被匯入的模組,在載入時就被編譯(無法做到按需編譯,降低首頁載入速度)。有些場景中,你可能希望根據條件匯入模組或者按需匯入模組,這時你可以使用動態匯入代替靜態匯入。import
可以像呼叫函數一樣來動態的匯入模組。以這種方式呼叫,將返回一個 promise
。import("./foo.js").then((module) => { const { default: foo, bar, baz } = module; console.log(foo); // [Function: foo]
console.log(bar); // 777
console.log(baz); // moment});複製程式碼
登入後複製
await
必須在帶有 async
的非同步函數中使用,否則會報錯:import("./foo.js").then((module) => {
const { default: foo, bar, baz } = module;
console.log(foo); // [Function: foo]
console.log(bar); // 777
console.log(baz); // moment
});
登入後複製
Top-level await
:const p = new Promise((resolve, reject) => { resolve(777);
});const result = await p;console.log(result);
// 777正常輸出
登入後複製
import
是靜態執行,所以不能使用表示式和變數,這些只有在執行時才能得到結果的語法結構。// 錯誤
import { 'b' + 'ar' } from './foo.js';
// 錯誤
let module = './foo.js';
import { bar } from module;
// 錯誤
if (x === 1) {
import { bar } from './foo.js';
} else {
import { foo } from './foo.js';
}
登入後複製
type
屬性設定為 module
用來告知瀏覽器將 script
標籤視為模組。<script type="module" src="./main.mjs"></script><script type="module"></script>
登入後複製
defer
的方式延遲你的 nomodule
指令碼: <script type="module">
console.log("模組情況下的");
</script>
<script src="./main.js" type="module" defer></script>
<script>
console.log("正常 script標籤");
</script>
登入後複製
nomodule
指令碼會被執行多次,而模組只會被執行一次: <script src="./foo.js"></script> <script src="./foo.js"></script>
<script type="module" src="./main.js"></script>
<script type="module" src="./main.js"></script>
<script type="module" src="./main.js"></script>
登入後複製
nomodule
指令碼會阻塞 HTML
解析。你可以通過新增 defer
屬性來解決此問題,該屬性是等到 HTML
解析完成之後才執行。defer
和 async
是一個可選屬性,他們只可以選擇其中一個,在 nomodule
指令碼下,defer
等到 HTML
解析完才會解析當前指令碼,而 async
會和 HTML
並行解析,不會阻塞 HTML
的解析,模組指令碼可以指定 async
屬性,但對於 defer
無效,因為模組預設就是延遲的。async
屬性,模組指令碼及其所有依賴項將於解析並行獲取,並且模組指令碼將在它可用時進行立即執行。Es Module
模組之前,必須先了解 Es Module
與 Commonjs
完全不同,它們有三個完全不同:CommonJS
模組輸出的是一個值的拷貝,Es Module
輸出的是值的參照;CommonJS
模組是執行時載入,Es Module
是編譯時輸出介面。CommonJS
模組的 require()
是同步載入模組,ES6 模組的import
命令是非同步載入,有一個獨立的模組依賴的解析階段。CommonJS
載入的是一個物件(即module.exports
屬性),該物件只有在指令碼執行完才會生成。而 Es Module
不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。Commonjs
輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。具體可以看上一篇寫的文章。Es Module
的執行機制與 CommonJS
不一樣。JS引擎
對指令碼靜態分析的時候,遇到模組載入命令import
,就會生成一個唯讀參照。等到指令碼真正執行時,再根據這個唯讀參照,到被載入的那個模組裡面去取值。換句話說,import
就是一個連線管道,原始值變了,import
載入的值也會跟著變。因此,Es Module
是動態參照,並且不會快取值,模組裡面的變數繫結其所在的模組。Module Record
) 封裝了關於單個模組(當前模組)的匯入和匯出的結構資訊。此資訊用於連結連線模組集的匯入和匯出。一個模組記錄包括四個欄位,它們只在執行模組時使用。其中這四個欄位分別是:Realm
: 建立當前模組的作用域;Environment
:模組的頂層繫結的環境記錄,該欄位在模組被連結時設定;Namespace
:模組名稱空間物件是模組名稱空間外來物件,它提供對模組匯出繫結的基於執行時屬性的存取。模組名稱空間物件沒有建構函式;HostDefined
:欄位保留,以按 host environments
使用,需要將附加資訊與模組關聯。import
繫結,這些繫結提供了對存在於另一個環境記錄中的目標繫結的間接存取。不可變繫結就是當前的模組引入其他的模組,引入的變數不能修改,這就是模組獨特的不可變繫結。
Construction
),根據地址查詢 js
檔案,通過網路下載,並且解析模組檔案為 Module Record
;Instantiation
),對模組進行範例化,並且分配記憶體空間,解析模組的匯入和匯出語句,把模組指向對應的記憶體地址;Evaluation
),執行程式碼,計算值,並且將值填充到記憶體地址中;loader
負責對模組進行定址及下載。首先我們修改一個入口檔案,這在 HTML
中通常是一個 <script type="module"></script>
的標籤來表示一個模組檔案。import
語句宣告,在 import
宣告語句中有一個 模組宣告識別符號(ModuleSpecifier
),這告訴 loader
怎麼查詢下一個模組的地址。模組記錄(Module Record)
,而每一個 模組記錄
包含了 JavaScript程式碼
、執行上下文
、ImportEntries
、LocalExportEntries
、IndirectExportEntries
、StarExportEntries
。其中 ImportEntries
值是一個 ImportEntry Records
型別,而 LocalExportEntries
、IndirectExportEntries
、StarExportEntries
是一個 ExportEntry Records
型別。ImportEntry Records
包含三個欄位 ModuleRequest
、ImportName
、LocalName
;ModuleSpecifier
);ModuleRequest
模組識別符號的模組匯出所需繫結的名稱。值 namespace-object
表示匯入請求是針對目標模組的名稱空間物件的;import
匯入的 ImportEntry Records
欄位的範例:匯入宣告 (Import Statement From) | 模組識別符號 (ModuleRequest) | 匯入名 (ImportName) | 本地名 (LocalName) |
---|---|---|---|
import React from "react"; | "react" | "default" | "React" |
import * as Moment from "react"; | "react" | namespace-obj | "Moment" |
import {useEffect} from "react"; | "react" | "useEffect" | "useEffect" |
import {useEffect as effect } from "react"; | "react" | "useEffect" | "effect" |
ExportEntry Records
包含四個欄位 ExportName
、ModuleRequest
、ImportName
、LocalName
,和 ImportEntry Records
不同的是多了一個 ExportName
。下面這張表記錄了使用 export
匯出的 ExportEntry Records
欄位的範例:
匯出宣告 | 匯出名 | 模組識別符號 | 匯入名 | 本地名 |
---|---|---|---|---|
export var v; | "v" | null | null | "v" |
export default function f() {} | "default" | null | null | "f" |
export default function () {} | "default" | null | null | "default" |
export default 42; | "default" | null | null | "default" |
export {x}; | "x" | null | null | "x" |
export {v as x}; | "x" | null | null | "v" |
export {x} from "mod"; | "x" | "mod" | "x" | null |
export {v as x} from "mod"; | "x" | "mod" | "v" | null |
export * from "mod"; | null | "mod" | all-but-default | null |
export * as ns from "mod"; | "ns | "mod" | all | null |
回到主題
只有當解析完當前的 Module Record
之後,才能知道當前模組依賴的是那些子模組,然後你需要 resolve
子模組,獲取子模組,再解析子模組,不斷的迴圈這個流程 resolving -> fetching -> parsing,結果如下圖所示:
靜態分析
,不會執行JavaScript程式碼,只會識別 export
和 import
關鍵字,所以說不能在非全域性作用域下使用 import
,動態匯入除外。loader
使用 Module Map
對全域性的 MOdule Record
進行追蹤、快取這樣就可以保證模組只被 fetch
一次,每個全域性作用域中會有一個獨立的 Module Map。MOdule Map 是由一個 URL 記錄和一個字串組成的key/value的對映物件。URL記錄是獲取模組的請求URL,字串指示模組的型別(例如。「javascript」)。模組對映的值要麼是模組指令碼,null(用於表示失敗的獲取),要麼是預留位置值「fetching(獲取中)」。
Module Record
被解析完後,接下來 JS 引擎需要把所有模組進行連結。JS 引擎以入口檔案的 Module Record
作為起點,以深度優先的順序去遞迴連結模組,為每個 Module Record
建立一個 Module Environment Record
,用於管理 Module Record
中的變數。Module Environment Record
中有一個 Binding
,這個是用來存放 Module Record
匯出的變數,如上圖所示,在該模組 main.js
處匯出了一個 count
的變數,在 Module Environment Record
中的 Binding
就會有一個 count
,在這個時候,就相當於 V8
的編譯階段,建立一個模組範例物件,新增相對應的屬性和方法,此時值為 undefined
或者 null
,為其分配記憶體空間。count.js
中使用了 import
關鍵字對 main.js
進行匯入,而 count.js
的 import
和 main.js
的 export
的變數指向的記憶體位置是一致的,這樣就把父子模組之間的關係連結起來了。如下圖所示:export
匯出的為父模組,import
引入的為子模組,父模組可以對變數進行修改,具有讀寫許可權,而子模組只有讀許可權。Es Module
中有5種狀態,分別為 unlinked
、linking
、linked
、evaluating
和 evaluated
,用迴圈模組記錄(Cyclic Module Records
)的 Status
欄位來表示,正是通過這個欄位來判斷模組是否被執行過,每個模組只執行一次。這也是為什麼會使用 Module Map
來進行全域性快取 Module Record
的原因了,如果一個模組的狀態為 evaluated
,那麼下次執行則會自動跳過,從而包裝一個模組只會執行一次。 Es Module
採用 深度優先
的方法對模組圖進行遍歷,每個模組只執行一次,這也就避免了死迴圈的情況了。深度優先搜尋演演算法(英語:Depth-First-Search,DFS)是一種用於遍歷或搜尋樹或圖的演演算法。這個演演算法會盡可能深地搜尋樹的分支。當節點v的所在邊都己被探尋過,搜尋將回溯到發現節點v的那條邊的起始節點。這一過程一直進行到已發現從源節點可達的所有節點為止。如果還存在未被發現的節點,則選擇其中一個作為源節點並重復以上過程,整個程序反覆進行直到所有節點都被存取為止。
// main.js
import { bar } from "./bar.js";
export const main = "main";
console.log("main");
// foo.js
import { main } from "./main.js";
export const foo = "foo";
console.log("foo");
// bar.js
import { foo } from "./foo.js";
export const bar = "bar";
console.log("bar");
登入後複製
node
執行 main.js
,得出以下結果:以上就是一文徹底搞定es6模組化的詳細內容,更多請關注TW511.COM其它相關文章!