讀懂CommonJS的模塊載入

来源:https://www.cnblogs.com/cherryvenus/archive/2018/09/29/9722304.html
-Advertisement-
Play Games

叨叨一會CommonJS Common這個英文單詞的意思,相信大家都認識,我記得有一個片語common knowledge是常識的意思,那麼CommonJS是不是也是類似於常識性的,大家都理解的意思呢?很明顯不是,這個常識一點都不常識。我最初認為commonJS是一個開源的JS庫,就是那種非常方便用 ...


叨叨一會CommonJS

Common這個英文單詞的意思,相信大家都認識,我記得有一個片語common knowledge是常識的意思,那麼CommonJS是不是也是類似於常識性的,大家都理解的意思呢?很明顯不是,這個常識一點都不常識。我最初認為commonJS是一個開源的JS庫,就是那種非常方便用的庫,裡面都是一些常用的前端方法,然而我錯得離譜,CommonJS不僅不是一個庫,還是一個看不見摸不著的東西,他只是一個規範!就像校紀校規一樣,用來規範JS編程,束縛住前端們。就和Promise一樣是一個規範,雖然有許多實現這些規範的開源庫,但是這個規範也是可以依靠我們的JS能力實現的。

CommonJs規範

那麼CommonJS規範了些什麼呢?要解釋這個規範,就要從JS的特性說起了。JS是一種直譯式腳本語言,也就是一邊編譯一邊運行,所以沒有模塊的概念。因此CommonJS是為了完善JS在這方面的缺失而存在的一種規範。

CommonJS定義了兩個主要概念:

  • require函數,用於導入模塊
  • module.exports變數,用於導出模塊

然而這兩個關鍵字,瀏覽器都不支持,所以我認為這是為什麼瀏覽器不支持CommonJS的原因。如果一定腰在瀏覽器上使用CommonJs,那麼就需要一些編譯庫,比如browserify來幫助哦我們將CommonJs編譯成瀏覽器支持的語法,其實就是實現require和exports。

那麼CommonJS可以用於那些方面呢?雖然CommonJS不能再瀏覽器中直接使用,但是nodejs可以基於CommonJS規範而實現的,親兒子的感覺。在nodejs中我們就可以直接使用require和exports這兩個關鍵詞來實現模塊的導入和導出。

Nodejs中CommomJS模塊的實現

require

導入,代碼很簡單,let {count,addCount}=require("./utils")就可以了。那麼在導入的時候發生了些什麼呢??首先肯定是解析路徑,系統給我們解析出一個絕對路徑,我們寫的相對對路徑是給我們看的,絕對路徑是給系統看的,畢竟絕對路徑辣麽長,看著很費力,尤其是當我們的的項目在N個文件夾之下的時候。所以require第一件事就是解析路徑。我們可以寫的很簡潔,只需要寫出相對路徑和文件名即可,連尾碼都可以省略,讓require幫我們去匹配去尋找。也就是說require的第一步是解析路徑獲取到模塊內容:

  • 如果是核心模塊,比如fs,就直接返回模塊
  • 如果是帶有路徑的如/,./等等,則拼接出一個絕對路徑,然後先讀取緩存require.cache再讀取文件。如果沒有加尾碼,則自動加尾碼然後一一識別。
    • .js 解析為JavaScript 文本文件
    • .json解析JSON對象
    • .node解析為二進位插件模塊
  • 首次載入後的模塊會緩存在require.cache之中,所以多次載入require,得到的對象是同一個。
  • 在執行模塊代碼的時候,會將模塊包裝成如下模式,以便於作用域在模塊範圍之內。
(function(exports, require, module, __filename, __dirname) {
// 模塊的代碼實際上在這裡
});

nodejs官方給出的解釋,大家可以參考下

module

說完了require做了些什麼事,那麼require觸發的module做了些什麼呢?我們看看用法,先寫一個簡單的導出模塊,寫好了模塊之後,只需要把需要導出的參數,加入module.exports就可以了。

let count=0
function addCount(){
    count++
}
module.exports={count,addCount}

然後根據require執行代碼時需要加上的,那麼實際上我們的代碼長成這樣:

(function(exports, require, module, __filename, __dirname) {
    let count=0
    function addCount(){
        count++
    }
    module.exports={count,addCount}
});

require的時候究竟module發生了什麼,我們可以在vscode打斷點:

根據這個斷點,我們可以整理出:

黃色圈出來的時require,也就是我們調用的方法

紅色圈出來的時Module的工作內容

Module._compile
Module.extesions..js
Module.load
tryMouduleLoad
Module._load
Module.runMain

藍色圈出來的是nodejs乾的事,也就是NativeModule,用於執行module對象的。

我們都知道在JS中,函數的調用時棧stack的方式,也就是先近後出,也就是說require這個函數觸發之後,圖中的運行時從下到上運行的。也就是藍色框最先運行。我把他的部分代碼扒出來,研究研究。

NativeModule原生代碼關鍵代碼,這一塊用於封裝模塊的。

NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
];

NativeModule觸發Module.runMain之後,我們的模塊載入開始了,我們按照從下至上的順序來解讀吧。

  • Module._load,就是新建一個module對象,然後將這個新對象放入Module緩存之中。
    var module = new Module(filename, parent); Module._cache[filename] = module;
  • tryMouduleLoad,然後就是新建的module對象開始解析導入的模塊內容
    module.load(filename);
  • 新建的module對象繼承了Module.load,這個方法就是解析文件的類型,然後分門別類地執行
  • Module.extesions..js這就幹了兩件事,讀取文件,然後準備編譯
  • Module._compile終於到了編譯的環節,那麼JS怎麼運行文本?將文本變成可執行對象,js有3種方法:
    • eval方法eval("console.log('aaa')")
    • new Function() 模板引擎
      let str="console.log(a)" new Function("aaa",str)
    • node執行字元串,我們用高級的vm
      let vm=require("vm") let a='console.log("a")' vm.runInThisContext(a)

      這裡Module用vm的方式編譯,首先是封裝一下,然後再執行,最後返回給require,我們就可以獲得執行的結果了。

      var wrapper = Module.wrap(content);
      var compiledWrapper = vm.runInThisContext(wrapper, {
          filename: filename,
          lineOffset: 0,
          displayErrors: true
      });

因為所有的模塊都是封裝之後再執行的,也就說導入的這個模塊,我們只能根據module.exports這一個對外介面來訪問內容。

總結一下

這些代碼看的人真的很暈,其實主要流程就是require之後解析路徑,然後觸發Module這一個類,然後Module_load的方法就是在當前模塊中創建一個新module的緩存,以保證下一次再require的時候可以直接返回而不用再次執行。然後就是這個新module的load方法載入並通過VM執行代碼返回對象給require

正因為是這樣編譯運行之後賦值給的緩存,所以如果export的值是一個參數,而不是函數,那麼如果當前參數的數值改變並不會引起export的改變,因為這個賦予export的參數是靜態的,並不會引起二次運行。

CommonJs模塊和ES6模塊的區別

使用場景

CommonJS因為關鍵字的局限性,因此大多用於伺服器端。而ES6的模塊載入,已經有瀏覽器支持了這個特性,因此ES6可以用於瀏覽器,如果遇到不支持ES6語法的瀏覽器,可以選擇轉譯成ES5。

語法差異

ES6也是一種JavaScript的規範,它和CommonJs模塊的區別,顯而易見,首先代碼就不一樣,ES6的導入導出很直觀importexport

commonJS ES6
支持的關鍵字 arguments,require,module,exports,__filename,__dirname import,export
導入 const path=require("path") import path from "path"
導出 module.exports = APP; export default APP
導入的對象 隨意修改 不能隨意修改
導入次數 可以隨意require,但是除了第一次,之後都是從模塊緩存中取得 在頭部導入

** 大家註意了!劃重點!nodejs是CommonJS的親兒子,所以有些ES6的特性並不支持,比如ES6對於模塊的關鍵字importexport,如果大家在nodejs環境下運行,就等著大紅的報錯吧~**

載入差異

除了語法上的差異,他們引用的模塊性質是不一樣的。雖然都是模塊,但是這模塊的結構差異很大。

在ES6中,如果大家想要在瀏覽器中測試,可以用以下代碼:

//utils.js
const x = 1;
export default x
<script type="module">
    import x from './utils.js';
    console.log(x);
    export default x
</script>

首先要給script一個type="module"表明這裡面是ES6的模塊,而且這個標簽預設是非同步載入,也就是頁面全部載入完成之後再執行,沒有這個標簽的話代碼不然無法運行哦。然後就可以直接寫import和export了。

ES6模塊導入的幾個問題:

  • 相同的模塊只能引入一次,比如x已經導入了,就不能再從utils中導入x
  • 不同的模塊引入相同的模塊,這個模塊只會在首次import中執行。
  • 引入的模塊就是一個值的引用,並且是動態的,改變之後其他的相關值也會變化
  • 引入的對象不可隨意斬斷鏈接,比如我引入的count我就不能修改他的值,因為這個是導入進來的,想要修改只能在count所在的模塊修改。但是如果count是一個對象,那麼可以改變對象的屬性,比如count.one=1,但是不可以count={one:1}

大家可以看這個例子,我寫了一個改變object值的小測試,大家會發現utils.js中的count初始值應該是0,但是運行了addCount所以count的值動態變化了,因此count的值變成了2

let count=0
function addCount(){
    count=count+2
}
export {count,addCount}
<script type="module">
    import {count,addCount} from './utils.js';
    //count=4//不可修改,會報錯
    addCount()
    console.log(count);
</script>

與之對比的是commonJS的模塊引用,他的特性是:

  • 上一節已經解釋了,模塊導出的固定值就是固定值,不會因為後期的修改而改變,除非不導出靜態值,而改成函數,每次調用都去動態調用,那麼每次值都是最新的了。
  • 導入的對象可以隨意修改,相當於只是導入模塊中的一個副本。

如果想要深入研究,大家可以參考下阮老師的ES6入門——Module 的載入實現

CommonJS模塊總結

CommonJS模塊只能運行再支持此規範的環境之中,nodejs是基於CommonJS規範開發的,因此可以很完美地運行CommonJS模塊,然後nodejs不支持ES6的模塊規範,所以nodejs的伺服器開發大家一般使用CommonJS規範來寫。

CommonJS模塊導入用require,導出用module.exports。導出的對象需註意,如果是靜態值,而且非常量,後期可能會有所改動的,請使用函數動態獲取,否則無法獲取修改值。導入的參數,是可以隨意改動的,所以大家使用時要小心。


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

-Advertisement-
Play Games
更多相關文章
  • 1.網頁內嵌動態生成的flash出錯,出錯信息如下: #25081850 myChartId::RendererManager Error >> There was an error rendering the chart. Enable FusionCharts JS debugMode for ...
  • 立即執行函數是表達式,只在括弧內可見。 例一 括弧內可見 例二 括弧外不可見 ...
  • video.js是一款很流行的html5視頻播放插件。很適合在移動端播放視頻(比如微信網頁),功能強大,且支持降級到flash,相容ie8。官網:http://videojs.com/ git&demo :http://files.cnblogs.com/files/stoneniqiu/video ...
  • ionic 是一個 HTML5 應用程式開發框架。 可以使用 HTML、CSS 和 Javascript 構建接近原生體驗的移動應用程式。具有速度快,界面現代化、美觀等特點。下麵一起看一下如何使用 安裝 確保已經安裝node 1. 下載Ionic and Cordova CLI. $ npm ins ...
  • What is Promise? Promise是一個構造函數,接受一個參數(Function),並且該參數接受兩個參數resolve和reject(分別表示非同步操作執行成功後的回調函數、執行失敗後的回調函數) 運行代碼,2秒後輸出“執行完成”。註意,這裡只是new了一個對象,並沒有調用它,所以我們 ...
  • 騰訊防水牆(滑動驗證碼)的簡單使用 https://007.qq.com ...
  • 騰訊地圖點擊地圖創建錨點(且只創建一個)同事創建錨點提示。 qq.maps.event.addListener(marker, 'click', function() { 44 info.open(); 45 info.setContent('單擊標記'); 47... ...
  • 今天分享一下快速使用jQuery+zepto.js的技巧,需要的記得收藏 1.jQuery的引入:本地下載jQuery(後面簡稱jq)的源文件,開發版本使用非min版,線上使用min版,zepto.js類似,同樣的一些基於jq的插件也是如此用法,如果使用requreJs也就是模塊化載入註意插件的依賴 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...