1. 程式人生 > 程式設計 >利用webpack理解CommonJS和ES Modules的差異區別

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

前言

問: 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 webpack@next 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:

<!DOCTYPE html>
<html lang="en">
 <head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1.0" />
  <title>Document</title>
 </head>
 <body>
  <script src="main.js"></script>
 </body>
</html>

在 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,{ enumerable: true,get: definition[key] }) 來對 exports 物件設定不同屬性的 getter

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

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

與 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 中打包的時間。

到此這篇關於利用webpack理解CommonJS和ES Modules的差異區別的文章就介紹到這了,更多相關webpack CommonJS和ES Modules 內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!