[JS] ES Modules的運作原理

来源:https://www.cnblogs.com/feixianxing/p/18415901/es-modules-deep-dive
-Advertisement-
Play Games

本文介紹了 ES Modules (ESM) 在瀏覽器環境中的運行原理,詳細闡述了 ESM 的三大載入步驟:構建、實例化、求值,並討論了其動態載入能力、迴圈依賴處理方式及與 CommonJS 的區別。 ...


ESM 通過 import 語句引入其它依賴,通過 export 語句導出模塊成員。

在瀏覽器環境中,<script> 可以通過聲明 type="module" 將一個 JS 文件標記為模塊,帶有 type="module" 聲明的<script> 類似於啟用了 defer,腳本文件的下載不會阻塞HTML渲染,代碼內容會被延後執行。

這篇文章僅討論瀏覽器環境下的 ESM。

概括

ES模塊的載入主要分為三個步驟:

  1. 構建 Construction
    • 找到入口文件;
    • 根據import語句遞歸構建依賴圖;
    • 下載模塊腳本文件,並文件轉換為 Module Record。
  2. 實例化 Instantiation
    • 為模塊導出的成員申請記憶體空間;
    • 建立importexport之間的鏈接;
  3. 求值 Evaluation
    • 運行模塊代碼;
    • 向記憶體中的成員填充實際的值。

模塊載入過程

步驟1 構建

構建過程的作用在於:構建依賴圖,以及瞭解各個模塊之間import/export的成員(靜態)。

路徑解析與文件下載

在代碼中我們使用的模塊通常是相對路徑,path resolver負責將相對路徑轉換為文件的絕對路徑,從而可以讓瀏覽器去下載模塊文件。

image-20240915173728406

轉換為模塊記錄

當模塊文件下載到瀏覽器本地之後,瀏覽器會對模塊文件進行靜態解析,從模塊代碼文件總結出一個模塊記錄(Module Record),可以理解為是模塊的元數據。

一個模塊記錄大致包含瞭如下信息:

  • 模塊文件的源代碼,以及根據源代碼構建的 AST;

  • 該模塊依賴的其它模塊;

  • 從其它模塊分別導入了哪些成員。

緩存機制

在瀏覽器中,一個標簽頁會維護一個模塊緩存映射表,它的 key 是模塊解析後的實際路徑,它的 value 是模塊記錄(Module Record)。

image-20240915181338503

當模塊文件的路徑被解析完成之後,它就會被添加到緩存中,而在“完成路徑解析”和“轉換為模塊記錄”這段時間內,它的 value 會被標記為 fetching

遞歸

image-20240915181952763

場景描述:

  1. 用戶訪問 https://www.example.com/index.html,返回的 HTML 文件包含模塊入口腳本文件
<script src="main.js" type="module"/>
  1. 相對路徑main.js被解析為絕對路徑 https://www.example.com/main.js,然後瀏覽器開始下載文件(此時這個模塊路徑已經被記錄到緩存了,標記為 fetching);
  2. 文件下載到瀏覽器本地之後,靜態解析代碼,捕獲import語句(import語句會被預設提升到代碼頂部),解析結果得到模塊記錄(Module Record),模塊記錄會被更新到緩存里;
  3. 模塊記錄包含依賴的其它模塊,此時瀏覽器會遞歸地解析它們的路徑,並下載它們的腳本文件(由上圖紅色箭頭標明)。

在這個過程中,網路請求下載腳本文件占據了大部分的時間開銷。

複雜的依賴關係可能導致初始化構建過程過久,影響首屏時間。

常用的優化手段是使用動態import,在運行時按需引入指定的模塊。

動態載入

語法

import('./dynamic-module.js').then(module => {
    console.log(module.default);
    console.log(module.xxx);
});

import(`./module-${moduleName}.js`).then(module => {
   // ... 
});

import函數的參數是模塊的文件路徑,返回一個 Promise 對象,通過 then 方法可以獲取到模塊對象。

模塊對象包含模塊導出的成員,預設導出使用default屬性獲取。

應用場景

  • 模塊懶載入,優化首屏時間;
  • 根據不同邏輯載入不同的模塊,所需的模塊是在運行時才確定的。

步驟2 實例化

實例化的主要作用是為模塊的state分配記憶體空間,此時僅作記憶體的分配,state的值在這一刻還不確定。

瀏覽器會以 深度優先後序遍歷 的方式遍歷依賴圖,為每一個模塊 export 的成員分配記憶體空間。

當模塊的所有 export 完成記憶體分配之後,會開始將 import 鏈接到相應的記憶體地址。

這意味著 export 導出的成員和 import 引入的成員指向同一處記憶體空間。基礎數據類型也是如此。

特點

  • 模塊內部更新 state,外部的state 也隨之變化(因為它們指向同一塊記憶體);
  • 模塊導出的 state只讀的

image-20240915225358286

這種現象和 CommonJS 存在很大區別,CommonJs 在導入模塊成員的時候,是對模塊的導出進行了拷貝

image-20240915225757132

這意味著在使用模塊導出的 state 時,要註意其數據是否是最新的,因為模塊內部和外部的 state 是相互獨立的,內部更新 state 並不會影響到外部的 state

不過這種情況一般比較少發生,我們很少直接導出一個基本數據類型,而是導出一個對象,對象內部再記錄這些基本數據類型。由於導出的是對象,只要模塊內部不要直接覆蓋整個對象,而是對對象的屬性進行更新,就不會有太大問題。

步驟3 求值

步驟1和2完成之後,模塊的成員已經完成了記憶體的分配,以及 import/export 之間的鏈接。

最後需要完成的,就是運行模塊代碼,並將成員的值填入先前分配的記憶體中。

模塊代碼中可能存在一些帶有副作用的代碼,為了避免每一次執行都會導致模塊的 exports 發生變化,模塊代碼只會被執行一次

迴圈依賴

迴圈依賴是所有模塊化方案都要討論的問題。

案例

image-20240916001635606

實際項目中,依賴圖是很複雜的,導致迴圈依賴的環可能包含了許多模塊。這裡僅討論最簡單的情況,即兩個模塊相互依賴對方。

CommonJS

假設main.js是入口文件。

main.js

const num = require('./a.js');
console.log(num);
exports.message = 'main';

a.js

const { message } = require('./main.js');
module.exports = 123;
setTimeout(()=>console.log(message), 0);

我們期待在main.js中輸出的num為123,而在a.js中輸出的message為 main;而實際運行結果是:

123
undefined
image-20240916003859389

CommonJS 的 require 函數是同步地載入模塊,並且一次性完成,不像ESM分為三個步驟。

如上圖,當代碼執行到 ① 時,執行require函數,解析路徑、記錄到緩存中、讀取模塊文件、執行模塊代碼(步驟②)。

由於 CommonJS 的同步特性,它不能直接運行於瀏覽器環境,這裡討論的 Node.js 環境下的模塊載入。

在執行步驟②的過程中,main.js導出的成員還沒有賦值,此時的module.exports是一個空對象。

但是由於 CommonJS 是在模塊的路徑解析階段就記錄了緩存,因此步驟②的require函數可以得到模塊main.jsmodule.exports,只不過此時的module.exports還是空對象。

由於它此時還是空對象,因此解構賦值出來的messageundefined

我們期待等步驟③這些同步代碼執行完成之後,message應該就會更新為main了,於是我們在a.js中,使用setTimeout來將任務推入巨集任務隊列中,延後執行。

但結果是,儘管main.js中的message被賦值了,a.js中的message也不會被更新。這是因為在導入的時候進行了拷貝,所以兩個message是相互獨立的。

image-20240916005943833

ESM

main.js

import num from './a.mjs';

console.log(num);

export const message = 'main';

a.js

import { message } from "./main.mjs";

export default 123;

setTimeout(()=>console.log(message), 0);

由於 ESM 的 import/export 是被鏈接到同一塊記憶體區域的,因此當 main.js 賦值message之後,a.js中的message 也會更新為 main

輸出結果

123
main

在瀏覽器環境下,為了使用 ESM 語法,入口腳本文件需要標明 type="module"

在 Node.js 環境下,為了表明文件是使用 ES 模塊化語法,需要將文件尾碼改為 .mjs,或者在 package.json 中配置 typemodule

總結

ES Modules (ESM) 是一種現代模塊化方案,具備以下特點和優勢:

  • 模塊化聲明

    • 使用 importexport 語句實現模塊的引入與導出。
    • 在瀏覽器中通過 <script type="module"> 標簽載入,不阻塞 HTML 渲染。
  • 載入過程

    1. 構建:遞歸構建依賴圖並下載模塊。
    2. 實例化:為導出的成員分配記憶體空間,建立 importexport 的鏈接。
    3. 求值:運行模塊代碼,填充記憶體中的成員值。
  • 與 CommonJS 對比

    特性 ESM CommonJS
    載入方式 非同步載入,不阻塞渲染 同步載入
    導入成員機制 共用同一記憶體空間,實時更新 拷貝機制,數據獨立
    瀏覽器支持 原生支持 <script type="module"> 僅支持 Node.js 環境
  • 優勢

    • 原生支持 動態載入
    • 解決 迴圈依賴 問題,確保模塊成員實時更新。

引用

[1] ES modules: A cartoon deep-dive - Mozilla Hacks - the Web developer blog


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文介紹如何通過模型服務靈積DashScope將 圖片轉換為向量 ,併入庫至向量檢索服務DashVector中進行向量檢索。 模型服務靈積DashScope,通過靈活、易用的模型API服務,讓各種模態模型的能力,都能方便的為AI開發者所用。通過靈積API,開發者不僅可以直接集成大模型的強大能力,也可 ...
  • 近日,第15屆中國資料庫技術大會(DTCC2024)在北京召開。大會以“自研創新 數智未來”為主題,重點圍繞向量資料庫與向量檢索技術實踐、數據治理與數據資產管理、雲原生資料庫開發與實踐、特定場景下的資料庫管理與優化、大數據平臺建設等內容展開分享和探討。 ...
  • 數據資產入表即數據資產會計核算,指的是把有價值的數據編製進資產負債表,作為企業沉澱的無形資產,讓數據要素的交易流通變得合規,數據價值可計算。 2023年8月21日,財政部發佈《企業數據資源相關會計處理暫行規定》,並於2024年1月1日開始實施,首次將數據資源納入企業會計核算體系,明確了數據資產入表的 ...
  • 引言 作為一名軟體行業的從業者,我已經在開源社區Remote工作了兩年時間。“技術運營” 是一個相對小眾的職業,所以如果你在這個行業工作,卻找不到自己熱情所在或擅長的事情,可能會感覺缺乏價值感(這一點我老闆也很認同)。 然而,如果你願意為自己的情懷買單,並全心投入其中,這確實是一件偉大的事情。 我並 ...
  • 作者:海豚調度研究隨筆 編輯整理:曾輝 前言 Apache DolphinScheduler 是一個優秀的分散式調度系統,廣泛應用於大數據處理和自動化任務管理中。本文詳細介紹瞭如何在Windows環境下搭建Apache DolphinScheduler的前後端開發環境。 包括從源碼的下載、環境配置、 ...
  • 由於web端和app公用一套菜單,而兩個項目的路徑是不同的,為解決這個問題,封裝了一套使用路由名稱作為跳轉路由的方法 1.在pages.json文件里pages對應的頁面配置里添加 routeName 欄位(自定義),我做的app裡面的菜單是後臺獲取的,所以這裡的value值對應的是後臺返回的頁面路 ...
  • Activity啟動模式 1. Activity啟動模式介紹 1.1 任務棧 在Android開發中,任務棧(Task Stack)是一個非常重要的概念,主要用於管理應用程式中的Activity及其啟動模式。它幫助開發者瞭解當用戶在不同應用之間切換,或者應用內部不同Activity之間跳轉時,系統如 ...
  • title: Nuxt Kit 中的上下文處理 date: 2024/9/16 updated: 2024/9/16 author: cmdragon excerpt: Nuxt Kit 提供的上下文處理工具,尤其是 useNuxt 和 tryUseNuxt,為模塊化開發提供了極大的便利。通過這些函 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...