首頁 >web前端 >js教程 >一文詳解es6中的模組化

一文詳解es6中的模組化

青灯夜游
青灯夜游轉載
2022-11-02 20:17:171727瀏覽

一文詳解es6中的模組化

CommonJs 有很多優秀的特性,下面我們再簡單的回顧一下:

  • 模組程式碼只在加載後運行;

  • 模組只能載入一次;

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

  • 支援循環依賴;

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

天下苦CommonJs 久矣


Es Module 的獨特之處在於,既可以透過瀏覽器原生載入,也可以與第三方載入器和建置工具一起載入.

支援 Es module 模組的瀏覽器可以從頂級模組載入整個依賴圖,且是非同步完成。瀏覽器會解析入口模組,確定依賴,並傳送對依賴模組的請求。這些檔案透過網路返回後,瀏覽器就會解析它們的依賴,,如果這些二級依賴還沒有載入,則會發送更多請求。

這個非同步遞歸載入過程會持續到整個應用程式的依賴圖都解析完成。解析完成依賴圖,引用程式就可以正式載入模組了。

Es Module 不僅借用了CommonJsAMD 的許多優秀特性,還增加了一些新行為:

  • #Es Module 預設在嚴格模式下執行;

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

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

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

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

##export 和import


    ##模組功能主要由兩個指令構成:
  • exports

    import

  • 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
    必須提供對外的介面:
  • <pre class="brush:js;toolbar:false;">// 1只是一个值,不是一个接口 export 1 // moment只是一个值为1的变量 const moment = 1 export moment // function和class的输出,也必须遵守这样的写法 function foo(x, y) { return x+y } export foo</pre>
import的基本使用

##使用export

#指令定義了模組的對外介面以後,其他js檔就可以透過
    import
  • 指令載入整個模組
    import {foo,age,nickname} form &#39;模块标识符&#39;
    模組識別碼可以是目前模組的相對路徑,也可以是絕對路徑,也可以是純字串,但不能是動態計算的結果,例如憑藉的字串。
    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 的异步函数中使用,否则会报错:
const p = new Promise((resolve, reject) => {
  resolve(111);
});

// SyntaxError: await is only valid in async functions and the top level bodies of modules
const result = await p;

console.log(result);
  • 而在模块中,你可以直接使用 Top-level await:
const p = new Promise((resolve, reject) => {
  resolve(777);
});

const result = await p;

console.log(result); // 777正常输出

import 的错误使用

  • 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 错误
import { &#39;b&#39; + &#39;ar&#39; } from &#39;./foo.js&#39;;

// 错误
let module = &#39;./foo.js&#39;;
import { bar } from module;

// 错误
if (x === 1) {
  import { bar } from &#39;./foo.js&#39;;
} else {
  import { foo } from &#39;./foo.js&#39;;
}

在浏览器中使用 Es Module


  • 在浏览器上,你可以通过将 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>

一文詳解es6中的模組化

  • 在浏览器中,引入相同的 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>

一文詳解es6中的模組化

模块的默认延迟


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

一文詳解es6中的模組化

  • deferasync 是一个可选属性,他们只可以选择其中一个,在 nomodule 脚本下,defer 等到 HTML 解析完才会解析当前脚本,而 async 会和 HTML 并行解析,不会阻塞 HTML 的解析,模块脚本可以指定 async 属性,但对于 defer 无效,因为模块默认就是延迟的。
  • 对于模块脚本,如果存在 async 属性,模块脚本及其所有依赖项将于解析并行获取,并且模块脚本将在它可用时进行立即执行。

Es Module 和 Commonjs 的区别


讨论 Es Module 模块之前,必须先了解 Es ModuleCommonjs 完全不同,它们有三个完全不同:

  • 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 是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

Es Module 工作原理的相关概念


  • 在学习工作原理之前,我们不妨来认识一下相关的概念。

Module Record

模块记录(Module Record) 封装了关于单个模块(当前模块)的导入和导出的结构信息。此信息用于链接连接模块集的导入和导出。一个模块记录包括四个字段,它们只在执行模块时使用。其中这四个字段分别是:

  • Realm: 创建当前模块的作用域;

  • Environment:模块的顶层绑定的环境记录,该字段在模块被链接时设置;

  • Namespace:模組命名空間物件是模組命名空間外來對象,它提供對模組導出綁定的基於執行時間屬性的存取。模組命名空間物件沒有建構子;

  • HostDefined:欄位保留,以按host environments 使用,需要將附加資訊與模組關聯。

Module Environment Record

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

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

Es Module 的解析流程


#在開始之前,我們先大概了解一下整個流程大概是怎麼樣的,先有一個大概的了解:

  • 階段一:建構(Construction),根據位址尋找js 檔案,透過網路下載,並且解析模組檔案為Module Record;

  • 階段二:實例化(Instantiation),對模組進行實例化,並且分配記憶體空間,解析模組的導入與導出語句,把模組指向對應的記憶體位址;

  • 階段三:運行(Evaluation),運行程式碼,計算值,並且將值填入記憶體位址;

Construction 建置階段

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

一文詳解es6中的模組化

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

一文詳解es6中的模組化

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

ImportEntry Records

    一個
  • ImportEntry Records 包含三個欄位ModuleRequest#、 ImportNameLocalName;
ModuleRequest: 一個模組識別碼(

ModuleSpecifier);

#ImportName: 由

ModuleRequest 模組識別碼的模組導出所需綁定的名稱。值namespace-object 表示導入請求是針對目標模組的命名空間物件的;

#LocalName: 用於從導入模組中從目前模組存取導入值的變數;

    詳情可參考下圖:
  • 一文詳解es6中的模組化
  • 下面這張表記錄了使用
  • import 匯入的ImportEntry Records 欄位的實例:
導入宣告(Import Statement Form)模組識別碼(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

ExportName: 此模組用於匯出時綁定的名稱。

  • 下面這張表記錄了使用export 匯出的ExportEntry Records 欄位的實例:

##" ns"mod"allnull#
  • 回到主題

  • 只有當解析完當前的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中的模組化

    • 好了,这篇文章到这也就结束了。

    原文地址:https://juejin.cn/post/7166046272300777508

    【推荐学习:javascript高级教程

    匯出宣告 匯出名稱 模組識別碼 匯入名稱 本機名稱
    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 * 來自 "mod"; null "mod" all-but-default null
    #export * as ns from "mod";

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

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