讀懂CommonJS的模塊加載

叨叨一會CommonJS

Common這個英文單詞的意思,相信你們都認識,我記得有一個詞組common knowledge是常識的意思,那麼CommonJS是否是也是相似於常識性的,你們都理解的意思呢?很明顯不是,這個常識一點都不常識。我最初認爲commonJS是一個開源的JS庫,就是那種很是方便用的庫,裏面都是一些經常使用的前端方法,然而我錯得離譜,CommonJS不只不是一個庫,仍是一個看不見摸不着的東西,他只是一個規範!就像校紀校規同樣,用來規範JS編程,束縛住前端們。就和Promise同樣是一個規範,雖然有許多實現這些規範的開源庫,可是這個規範也是能夠依靠咱們的JS能力實現的。javascript

CommonJs規範

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

CommonJS定義了兩個主要概念:前端

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

然而這兩個關鍵字,瀏覽器都不支持,因此我認爲這是爲何瀏覽器不支持CommonJS的緣由。若是必定腰在瀏覽器上使用CommonJs,那麼就須要一些編譯庫,好比browserify來幫助哦咱們將CommonJs編譯成瀏覽器支持的語法,其實就是實現require和exports。java

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

Nodejs中CommomJS模塊的實現

require

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

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

nodejs官方給出的解釋,你們能夠參考下編程

module

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

let count=0
function addCount(){
    count++
}
module.exports={count,addCount}
複製代碼

而後根據require執行代碼時須要加上的,那麼實際上咱們的代碼長成這樣:api

(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。導出的對象需注意,若是是靜態值,並且很是量,後期可能會有所改動的,請使用函數動態獲取,不然沒法獲取修改值。導入的參數,是能夠隨意改動的,因此你們使用時要當心。

相關文章
相關標籤/搜索