[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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...