首頁  >  文章  >  web前端  >  一文徹底搞定es6模組化

一文徹底搞定es6模組化

藏色散人
藏色散人轉載
2023-02-17 11:17:592485瀏覽

前情回顧

  • 在上篇文章中我們講到了 CommonJs,如果還沒看,可以查找本文章所在的專欄進行學習。
  • CommonJs 有很多優秀的功能,下面我們再簡單的回顧一下:
  • 模組程式碼只在載入後運行;

  • 模組只能載入一次;

  • 模組可以要求載入其他模組;

  • 支持循環依賴;

  • 模組可以定義公共介面,其他模組可以基於這個公共介面觀察與互動;

天下苦CommonJs 久矣

  • Es Module 的獨特之處在於,既可以透過瀏覽器原生載入,也可以與第三方載入器和建置工具一起載入。
  • 支援 Es module 模組的瀏覽器可以從頂級模組載入整個依賴圖,且是非同步完成。瀏覽器會解析入口模組,確定依賴,並傳送對依賴模組的請求。這些檔案透過網路返回後,瀏覽器就會解析它們的依賴,,如果這些二級依賴還沒有載入,則會發送更多請求。
  • 這個非同步遞歸載入過程會持續到整個應用程式的依賴圖都解析完成。解析完成依賴圖,引用程式就可以正式載入模組了。
  • Es Module 不只借用了CommonJsAMD 的許多優秀特性,還增加了一些新行為:
  • #Es Module 預設在嚴格模式下執行;

  • #Es Module 不共享全域命名空;

  • Es Module 頂層的this# 的值是undefined(常規腳本是window# );

  • 模組中的var 宣告不會加入到window 物件;

  • ##Es Module 是非同步載入和執行的;

export 和import

    模組功能主要由兩個指令構成:
  • exportsimport
  • export指令用來規定模組的對外接口,import指令用來輸入其他模組提供的功能。
export的基本使用

    導出的基本形式:
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 };
export 的錯誤使用

    導出語句必須在模組頂層,不能嵌套在某個區塊中:
if(true){
export {...};
}
  • #export 必須提供對外的介面:
// 1只是一个值,不是一个接口export 1// moment只是一个值为1的变量const moment = 1export moment// function和class的输出,也必须遵守这样的写法function foo(x, y) {    return x+y
}export foo复制代码
import的基本使用

    使用
  • export 指令定義了模組的對外介面以後,其他js檔案就可以透過import 指令加載整個模組
import {foo,age,nickname} from '模块标识符'
    模組識別碼可以是目前模組的相對路徑,也可以是絕對路徑,也可以是純字串,但不能是動態計算的結果,例如憑藉的字串。
  • import 指令後面接受一個花括弧,裡面指定要從其他模組匯入的變數名稱,而且變數名稱必須與被匯入模組的對外介面的名稱相同。
  • 對於導入的變數不能對其重新賦值,因為它是一個只讀介面,如果是一個物件,可以對這個物件的屬性重新賦值。導出的模組可以修改值,導入的變數也會跟著改變。

一文徹底搞定es6模組化#

  • 从上图可以看得出来,对象的属性被重新赋值了,而变量的则报了 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 导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。
  • 关键字 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

  • 在经典脚本中使用 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是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 错误
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';
}

在浏览器中使用 Es Module

  • 在浏览器上,你可以通过将 type 属性设置为 module 用来告知浏览器将 script 标签视为模块。
<script></script><script></script>
  • 模块默认情况下是延迟的,因此你还可以使用 defer 的方式延迟你的 nomodule 脚本:
  <script>      
  console.log("模块情况下的");
    </script>    
    <script></script>
    <script>
      console.log("正常 script标签");    
      </script>

一文徹底搞定es6模組化

  • 在浏览器中,引入相同的 nomodule 脚本会被执行多次,而模块只会被执行一次:
    <script></script>    <script></script>

    <script></script>
    <script></script>
    <script></script>

一文徹底搞定es6模組化

模块的默认延迟

  • 默认情况下,nomodule 脚本会阻塞 HTML 解析。你可以通过添加 defer 属性来解决此问题,该属性是等到 HTML 解析完成之后才执行。

一文徹底搞定es6模組化

  • deferasync 是一個可選屬性,他們只可以選擇其中一個,在nomodule 腳本下,defer 等到HTML 解析完才會解析目前腳本,而async 會和HTML 並行解析,不會阻塞HTML 的解析,模組腳本可以指定async 屬性,但對於defer 無效,因為模組預設就是延遲的。
  • 對於模組腳本,如果存在 async 屬性,模組腳本及其所有依賴項將於解析並行獲取,並且模組腳本將在它可用時進行立即執行。

Es Module 和Commonjs 的差異

  • 討論Es Module 模組之前,必須先了解Es ModuleCommonjs 完全不同,它們有三個完全不同:
  1. CommonJS 模組輸出的是一個值的拷貝,Es Module輸出的是值的參考;
  2. CommonJS 模組是運行時加載,Es Module 是編譯時輸出介面。
  3. CommonJS 模組的require() 是同步加載模組,ES6 模組的import命令是異步加載,有一個獨立的模組依賴的解析階段。
  • 第二個差異是因為CommonJS 載入的是物件(即module.exports屬性),該物件只有在腳本執行完才會生成。而 Es Module 不是對象,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會產生。
  • Commonjs 輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就會影響不到這個值。具體可以看​​上一篇寫的文章。
  • Es Module 的運作機制與 CommonJS 不一樣。 JS引擎 對腳本靜態分析的時候,遇到模組載入指令import,就會產生一個唯讀參考。等到腳本真正執行時,再根據這個唯讀引用,到被載入的那個模組裡面去取值。換句話說,import就是一個連接管道,原始值改變了,import 載入的值也會跟著變。因此,Es Module 是動態引用,並且不會快取值,模組裡面的變數綁定其所在的模組。

Es Module 工作原理的相關概念

  • 在學習工作原理之前,我們不妨來認識一下相關的概念。

Module Record

  • 模組記錄(Module Record) 封裝了關於單一模組(目前模組)的匯入和匯出的結構資訊。此資訊用於連結連接模組集的匯入和匯出。一個模組記錄包括四個字段,它們只在執行模組時使用。其中這四個欄位分別是:
  1. Realm: 建立目前模組的作用域;
  2. Environment:模組的頂層綁定的環境記錄,該字段在模組被鏈接時設置;
  3. Namespace:模組命名空間對像是模組命名空間外來對象,它提供對模組導出綁定的基於運行時屬性的存取。模組命名空間物件沒有建構子;
  4. HostDefined:欄位保留,以按 host environments 使用,需要將附加資訊與模組關聯。

Module Environment Record

  • 模組環境記錄是一種宣告式環境記錄,用來表示ECMAScript模組的外部作用域。除了普通的可變和不可變綁定之外,模組環境記錄還提供了不可變的import 綁定,這些綁定提供了對存在於另一個環境記錄中的目標綁定的間接存取。

不可變綁定就是當前的模組引入其他的模組,引入的變數不能修改,這就是模組獨特的不可變綁定。

Es Module 的解析流程

  • 在開始之前,我們先大概了解一下整個流程大概是怎麼樣的,先有一個大概的了解:
  1. 階段一:建構(Construction),根據位址尋找js 檔案,透過網路下載,並且解析模組檔案為Module Record;
  2. 階段二:實例化(Instantiation),對模組進行實例化,並且分配記憶體空間,解析模組的導入與導出語句,把模組指向對應的記憶體位址;
  3. 階段三:運行(Evaluation),運行程式碼,計算值,並且將值填入記憶體位址;

Construction 建置階段

  • loader 負責對模組進行尋址及下載。首先我們修改一個入口檔,這在 HTML 中通常是一個 <script type="module"></script> 的標籤來表示模組檔。

一文徹底搞定es6模組化

  • 模組繼續透過import語句宣告,在import宣告語句中有一個模組宣告標識符(ModuleSpecifier),這告訴loader 怎麼找出下一個模組的位址。

一文徹底搞定es6模組化

  • 每一個模組標識號對應一個模組記錄(Module Record),而每一個模組記錄 包含了JavaScript程式碼執行上下文ImportEntriesLocalExportEntriesIndirectExportEntriesStarExportEntries 。其中ImportEntries 值是一個ImportEntry Records 類型,而LocalExportEntriesIndirectExportEntriesStarExportEntries 是一個 ExportEntry Records 類型。

ImportEntry Records

  • 一個ImportEntry Records 包含三個欄位ModuleRequestImportNameLocalName;
  1. ModuleRequest: 一個模組識別碼(ModuleSpecifier);
  2. ImportName: 由ModuleRequest 模組識別碼的模組導出所需綁定的名稱。值namespace-object 表示導入請求是針對目標模組的命名空間物件的;
  3. #LocalName: 用於從導入模組中從目前模組存取導入值的變數;
  • 詳情可參考下圖:一文徹底搞定es6模組化
  • 下面這張表記錄了使用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

  • 一個ExportEntry Records 包含四個欄位ExportNameModuleRequestImportNameLocalName,和ImportEntry Records不同的是多了一個ExportName
  1. ExportName: 此模組用於匯出時綁定的名稱。
  • 下面這張表記錄了使用export 匯出的ExportEntry Records 欄位的實例:

    "v""default"nulldefaultnulldefault"x""x""x""x"null"ns
    匯出聲明 匯出名 #模組識別碼 #匯入名 本機名稱
    export var v;
    #null null "v" export default function f() {}
    null null "f" export default function () {}"default"
    null "" export default 42;"default"
    null #"" 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";
    "mod"######all######null############
  • 回到主題

  • 只有當解析完當前的Module Record 之後,才能知道當前模組依賴的是那些子模組,然後你需要resolve 子模組,取得子模組,再解析子模組,不斷的循環這個流程resolving -> fetching -> parsing,結果如下圖所示:

一文徹底搞定es6模組化

  • 這個過程也稱為靜態分析,不會執行JavaScript程式碼,只會辨識exportimport 關鍵字,所以說不能在非全域作用域下使用import,動態導入除外。
  • 如果多個檔案同時依賴一個檔案呢,這會不會造成死循環,答案是不會的。
  • loader 使用Module Map 對全域的MOdule Record 進行追蹤、快取這樣就可以保證模組只被fetch 一次,每個全域作用域中會有一個獨立的Module Map。

MOdule Map 是由一個 URL 記錄和一個字串組成的key/value的對應物件。 URL記錄是取得模組的請求URL,字串指示模組的類型(例如。「javascript」)。模組映射的值要么是模組腳本,null(用於表示失敗的獲取),要么是佔位符值“fetching(獲取中)”。

一文徹底搞定es6模組化

linking 連結階段

  • #在所有Module Record 被解析完後,接下來JS 引擎需要把所有模組進行連結。 JS 引擎以入口檔案的Module Record 作為起點,以深度優先的順序去遞歸連結模組,為每個Module Record 建立一個Module Environment Record,用於管理Module Record 中的變數。

一文徹底搞定es6模組化

  • Module Environment Record 中有一個Binding,這個是用來存放#Module Record 導出的變數,如上圖所示,在該模組main.js 處導出了一個count 的變數,在Module Environment Record 中的Binding 就會有一個count,在這個時候,就相當於V8 的編譯階段,創建一個模組實例物件,添加相對應的屬性和方法,此時值為undefinednull,為其分配記憶體空間。
  • 而在子模組count.js 中使用了import 關鍵字對main.js 進行導入,而count. jsimportmain.jsexport 的變數指向的記憶體位置是一致的,這樣就把父子模組之間的關係鏈接起來了。如下圖所示:

一文徹底搞定es6模組化

  • 需要注意的是,我们称 export 导出的为父模块,import 引入的为子模块,父模块可以对变量进行修改,具有读写权限,而子模块只有读权限。

Evaluation 求值阶段

  • 在模块彼此链接完之后,执行对应模块文件中顶层作用域的代码,确定链接阶段中定义变量的值,放入内存中。

Es module 是如何解决循环引用的

  • Es Module 中有5种状态,分别为 unlinkedlinkinglinkedevaluatingevaluated,用循环模块记录(Cyclic Module Records)的 Status 字段来表示,正是通过这个字段来判断模块是否被执行过,每个模块只执行一次。这也是为什么会使用 Module Map 来进行全局缓存 Module Record 的原因了,如果一个模块的状态为 evaluated,那么下次执行则会自动跳过,从而包装一个模块只会执行一次。 Es Module 采用 深度优先 的方法对模块图进行遍历,每个模块只执行一次,这也就避免了死循环的情况了。

深度优先搜索算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会尽可能深地搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。

一文徹底搞定es6模組化

  • 看下面的例子,所有的模块只会运行一次:
// 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模組化

以上是一文徹底搞定es6模組化的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除