CommonJs
,如果還沒看,可以查找本文章所在的專欄進行學習。 CommonJs
有很多優秀的功能,下面我們再簡單的回顧一下:模組程式碼只在載入後運行;
模組只能載入一次;
模組可以要求載入其他模組;
支持循環依賴;
模組可以定義公共介面,其他模組可以基於這個公共介面觀察與互動;
Es Module
的獨特之處在於,既可以透過瀏覽器原生載入,也可以與第三方載入器和建置工具一起載入。 Es module
模組的瀏覽器可以從頂級模組載入整個依賴圖,且是非同步完成。瀏覽器會解析入口模組,確定依賴,並傳送對依賴模組的請求。這些檔案透過網路返回後,瀏覽器就會解析它們的依賴,,如果這些二級依賴還沒有載入,則會發送更多請求。 Es Module
不只借用了CommonJs
和AMD
的許多優秀特性,還增加了一些新行為:#Es Module
預設在嚴格模式下執行;
#Es Module
不共享全域命名空;
Es Module
頂層的this
# 的值是undefined
(常規腳本是window
# );
模組中的var
宣告不會加入到window
物件;
##Es Module 是非同步載入和執行的;
和
import。
指令用來規定模組的對外接口,
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 };
輸出的變數就是本來的名字,但是可以使用
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 {...}; }
必須提供對外的介面:
// 1只是一个值,不是一个接口export 1// moment只是一个值为1的变量const moment = 1export moment// function和class的输出,也必须遵守这样的写法function foo(x, y) { return x+y }export foo复制代码
指令定義了模組的對外介面以後,其他js檔案就可以透過
import 指令加載整個模組
import {foo,age,nickname} from '模块标识符'
指令後面接受一個花括弧,裡面指定要從其他模組匯入的變數名稱,而且變數名稱必須與被匯入模組的對外介面的名稱相同。
#
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></script><script></script>
defer
的方式延迟你的 nomodule
脚本:<script> console.log("模块情况下的"); </script> <script></script> <script> console.log("正常 script标签"); </script>
nomodule
脚本会被执行多次,而模块只会被执行一次:<script></script> <script></script> <script></script> <script></script> <script></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; | ||||
#null | null | "v" | export default function f() {} | |
null | null | "f" | export default function () {}"default" | |
null | " | default" | export default 42;"default" | |
null | #" | default" | export {x}; | |
null | null | "x" | export {v as x}; | |
null | null | "v" | export {x} from "mod"; | |
"mod" | "x" | null | export {v as x} from "mod"; | |
"mod" | "v" | null | #export * from "mod"; | |
"mod" | all-but-default | null | #export * as ns from "mod"; |
回到主題
只有當解析完當前的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模組化的詳細內容。更多資訊請關注PHP中文網其他相關文章!