歡迎您光臨本站 註冊首頁

利用webpack理解CommonJS和ES Modules的差異區別

←手機掃碼閱讀     retouched @ 2020-06-17 , reply:0

前言
 

問: CommonJS 和 ES Modules 中模塊引入的區別?

CommonJS 輸出的是一個值的拷貝;ES Modules 生成一個引用,等到真的需要用到時,再到模塊裡面去取值,模塊裡面的變量,綁定其所在的模塊。

我相信很多人已經把這個答案背得滾瓜爛熟,好,那繼續提問。
 

問:CommonJS 輸出的值是淺拷貝還是深拷貝?
 

問:你能模擬實現 ES Modules 的引用生成嗎?
 

對於以上兩個問題,我也是感到一臉懵逼,好在有 webpack 的幫助,作為一個打包工具,它讓 ES Modules, CommonJS 的工作流程瞬間清晰明瞭。
 

準備工作
 

初始化項目,並安裝 beta 版本的 webpack 5,它相較於 webpack 4 做了許多優化:對 ES Modules 的支持度更高,打包後的代碼也更精簡。
 

  $ mkdir demo && cd demo  $ yarn init -y  $ yarn add [email protected] webpack-cli  # or yarn add [email protected] webpack-cli

 

早在 webpack4 就已經引入了無配置的概念,既不需要提供 webpack.config.js 文件,它會默認以 src/index.js 為入口文件,生成打包後的 main.js 放置於 dist 文件夾中。
 

確保你擁有以下目錄結構:
 

  ├── dist  │  └── index.html  ├── src  │  └── index.js  ├── package.json  └── yarn.lock

 

在 index.html 中引入打包後的 main.js:
 

  Document

 

在 package.json 中添加命令腳本:
 

  "scripts": {   "start": "webpack"  },

 

運行無配置打包:
 

  $ yarn start

 

終端會提示:
 

WARNING in configuration
 The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
 You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
 

webpack 要求用戶在打包時必須提供 mode 選項,來指明打包後的資源用於開發環境還是生產環境,從而讓 webpack 相應地使用其內置優化,默認為 production(生產環境)。
 

我們將其設置為 none 來避免默認行為帶來的干擾,以便我們更好的分析源碼。
 修改 package.json:
 

  "scripts": {   "start": "webpack --mode=none"  },

 

重新運行,webpack 在 dist 目錄下生成了打包後的 main.js,由於入口文件是空的,所以 main.js 的源碼只有一個 IIFE(立即執行函數),看似簡單,但它的地位卻極其重要。
 

  (() => {   // webpackBootstrap  })();

 

我們知道無論在 CommonJS 或 ES Modules 中,一個文件就是一個模塊,模塊之間的作用域相互隔離,且不會汙染全局作用域。此刻 IIFE 就派上了用場,它將一個文件的全部 JS 代碼包裹起來,形成閉包函數,不僅起到了函數自執行的作用,還能保證函數間的作用域不會互相汙染,並且在閉包函數外無法直接訪問內部變量,除非內部變量被顯式導出。
 

  var name = "webpack";    (() => {   var name = "parcel";   var age = 18;   console.log(name); // parcel  })();    console.log(name); // webpack  console.log(age); // ReferenceError: age is not defined

 

引用 vs 拷貝
 

接下里進入實踐部分,涉及源碼的閱讀,讓我們深入瞭解 CommonJS 和 ES Modules 的差異所在。
 

CommonJS
 

新建 src/counter.js
 

  let num = 1;    function increase() {   return num++;  }    module.exports = { num, increase };

 

修改 index.js
 

  const { num, increase } = require("./counter");    console.log(num);  increase();  console.log(num);

 

如果你看過前面敘述,毫無疑問,打印 1 1.
 

so why?我們查看 main.js,那有我們想要的答案,去除無用的註釋後如下:
 

  (() => {   var __webpack_modules__ = [    ,    module => {     let num = 1;       function increase() {      return num++;     }       module.exports = { num, increase };    },   ];     var __webpack_module_cache__ = {};     function __webpack_require__(moduleId) {    // Check if module is in cache    if (__webpack_module_cache__[moduleId]) {     return __webpack_module_cache__[moduleId].exports;    }    // Create a new module (and put it into the cache)    var module = (__webpack_module_cache__[moduleId] = {     exports: {},    });      // Execute the module function    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);      return module.exports;   }     (() => {    const { num, increase } = __webpack_require__(1);      console.log(num);    increase();    console.log(num);   })();  })();

 

可以簡化為:
 

  (() => {   var __webpack_modules__ = [...];   var __webpack_module_cache__ = {};     function __webpack_require__(moduleId) {...}     (() => {    const { num, increase } = __webpack_require__(1);      console.log(num);    increase();    console.log(num);   })();  })();

 

最外層是一個 IIFE,立即執行。
 

__webpack_modules__,它是一個數組,第一項為空,第二項是一個箭頭函數並傳入 module 參數,函數內部包含了 counter.js 中的所有代碼。
 

__webpack_module_cache__ 緩存已經加載過的模塊。
 

function __webpack_require__(moduleId) {...} 類似於 require(),他會先去 __webpack_module_cache__ 中查找此模塊是否已經被加載過,如果被加載過,直接返回緩存中的內容。否則,新建一個 module: {exports: {}},並設置緩存,執行模塊函數,最後返回 module.exports
 

最後遇到一個 IIFE,它將 index.js 中的代碼包裝在內,並執行 __webpack_require__(1),導出了 num 和 increase 供 index.js 使用。
 

這裡的關鍵點在於 counter.js 中的 module.exports = { num, increase };,等同於以下寫法:
 

  module.exports = {   num: num,   increase: increase,  };

 

num 屬於基本類型,假設其內存地址指向 n1,當它被 賦值 給 module.exports['num'] 時,module.exports['num'] 已經指向了一個新的內存地址 n2,只不過其值同樣為 1,但和 num 已是形同陌路,毫不相干。
 

  let num = 1;  // mun 相當於 module.exports['num']  mun = num;    num = 999;  console.log(mun); // 1

 

increase 是一個函數,屬於引用類型,即 increase 只作為一個指針,當它被賦值給 module.exports['increase'] 時,只進行了指針的複製,是 淺拷貝(基本類型沒有深淺拷貝的說法),其內存地址依舊指向同一塊數據。所以本質上 module.exports['increase'] 就是 increase,只不過換個名字。
 

而由於詞法作用域的特性,counter.js 中 increase() 修改的 num 變量在函數聲明時就已經綁定不變了,永遠綁定內存地址指向 n1 的 num.

JavaScript 採用的是詞法作用域,它規定了函數內訪問變量時,查找變量是從函數聲明的位置向外層作用域中查找,而不是從調用函數的位置開始向上查找

  function foo() {   var x = 10;   console.log(x);  }  function bar(f) {   var x = 20;   f();  }  bar(foo); // 10

 

調用 increase() 並不會影響內存地址指向 n2 的 num,這也就是為什麼打印 1 1 的理由。
 

ES Modules
 

分別修改 counter.js 和 index.js,這回使用 ES Modules.
 

  let num = 1;    function increase() {   return num++;  }    export { num, increase };

 

  import { num, increase } from "./counter";    console.log(num);  increase();  console.log(num);

 

很明顯,打印 1 2.
 

老規矩,查看 main.js,刪除無用的註釋後如下:
 

  (() => {   "use strict";   var __webpack_modules__ = [    ,    (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {     __webpack_require__.d(__webpack_exports__, {      num: () => /* binding */ num,      increase: () => /* binding */ increase,     });     let num = 1;       function increase() {      return num++;     }    },   ];     var __webpack_module_cache__ = {};     function __webpack_require__(moduleId) {} // 筆者注:同一個函數,不再展開     /* webpack/runtime/define property getters */   (() => {    __webpack_require__.d = (exports, definition) => {     for (var key in definition) {      if (       __webpack_require__.o(definition, key) &&       !__webpack_require__.o(exports, key)      ) {       Object.defineProperty(exports, key, {        enumerable: true,        get: definition[key],       });      }     }    };   })();     /* webpack/runtime/hasOwnProperty shorthand */   (() => {    __webpack_require__.o = (obj, prop) =>     Object.prototype.hasOwnProperty.call(obj, prop);   })();     (() => {    var _counter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);      console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);    (0, _counter__WEBPACK_IMPORTED_MODULE_0__.increase)();    console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);   })();  })();

 

經過簡化,大致如下:
 

  (() => {   "use strict";   var __webpack_modules__ = [...];   var __webpack_module_cache__ = {};     function __webpack_require__(moduleId) {...}     (() => {    __webpack_require__.d = (exports, definition) => {...};   })();     (() => {    __webpack_require__.o = (obj, prop) => {...}   })();     (() => {    var _counter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);      console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);    (0, _counter__WEBPACK_IMPORTED_MODULE_0__.increase)();    console.log(_counter__WEBPACK_IMPORTED_MODULE_0__.num);   })();  })();

 

首先查看兩個工具函數:__webpack_require__.o 和 __webpack_require__.d。
 

__webpack_require__.o 封裝了 Object.prototype.hasOwnProperty.call(obj, prop) 的操作。
 

__webpack_require__.d 則是通過 Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }) 來對 exports 對象設置不同屬性的 getter
 

隨後看到了熟悉的 __webpack_modules__,它的形式和上一節差不多,最主要的是以下這段代碼:
 

  __webpack_require__.d(__webpack_exports__, {   num: () => /* binding */ num,   increase: () => /* binding */ increase,  });

 

與 CommonJS 不同,ES Modules 並沒有對 module.exports 直接賦值,而是將值作為箭頭函數的返回值,再把箭頭函數賦值給 module.exports,之前我們提過詞法作用域的概念,即這裡的 num() 和 increase() 無論在哪裡執行,返回的 num 變量和 increase 函數都是 counter.js 中的。
 

在遇到最後一個 IIFE 時,調用 __webpack_require__(1),返回 module.exports 並賦值給 _counter__WEBPACK_IMPORTED_MODULE_0__,後續所有的屬性獲取都是使用點操作符,這觸發了對應屬性的 get 操作,於是執行函數返回 counter.js 中的值。
 

所以打印 1 2.
 

懂了詞法作用域的原理,就可以實現一個”乞丐版“的 ES Modules:
 

  function my_require() {   var module = {    exports: {},   };   let counter = 1;     function add() {    return counter++;   }     module.exports = { counter: () => counter, add };   return module.exports;  }    var obj = my_require();    console.log(obj.counter()); // 1  obj.add();  console.log(obj.counter()); // 2

 

總結
 

多去看源碼,會有不少的收穫,這是一個思考的過程。
 ES Modules 已經寫入了 ES2020 規範中,意味著瀏覽器原生支持 import 和 export,有興趣的小夥伴可以試試 Snowpack,它能直接 export 第三方庫供瀏覽器使用,省去了 webpack 中打包的時間。


[retouched ] 利用webpack理解CommonJS和ES Modules的差異區別已經有185次圍觀

http://coctec.com/docs/javascript/show-post-238847.html