CommonJS和ES6 Module 模塊規範原理淺析

這個兩個規範曾經困擾過我,由於他們的關鍵詞都很像:import/export/export default/require/module.exports... 老是傻傻分不清楚。javascript

關於他們之間的區別只是據說一個動態加載、一個靜態加載;一個導出副本,一個導出引用。這又是什麼意思,爲何?java

爲了解惑,我使用了webpack+babel將這兩種規範的模塊代碼打包成ES5,看看他們到底都是怎麼作的。本文先解釋他們各自的概念、表現,再來分析代碼、剖析原理。(使用babel主要是爲了把ES6 Module轉成ES5看它內部是怎麼工做的(更新:webpack原生支持import/export,無需使用babel轉譯))node

爲何要模塊化

若是不採用模塊化,從body底部引入js文件時必需要確保引用順序正確,不然將沒法運行。而當js文件數量太大的時候,文件之間的依賴關係存在不肯定性,沒法保證順序正確,所以出現了模塊化。webpack

打包代碼的webpack配置

最小化的配置以下,主要是要配置source-map,和開發模式,這樣打包出來的代碼就是普通的代碼,沒有壓縮、沒有用eval包含,比較易讀。而後就能夠建立兩個js文件各類嘗試了。(本次使用的 webpack 版本爲:4.43.0)es6

// webpack.config.js
{
// ...
  mode: 'development',
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules)/,
        loader: 'babel-loader',
      }
    ]
  }
// ...
}

// .babelrc
{
  "presets": [
    "@babel/preset-env"
  ]
}
複製代碼

CommonJS規範

CommonJS規範的實現很是簡單,放在前面來講。web

概念

CommonJS規範是Node.js處理模塊的標準。npm生態就是創建在CommonJS標準之上的。npm

能夠導出的有:變量、function、對象等。 如今使用頻率最高。它使用同步加載的方式將模塊一次性加載出來。瀏覽器

使用示例:緩存

/* 導出 */
// 直接掛到exports上
exports.uppercase = str => str.toUpperCase();
// 掛到module.exports上
module.exports.a = 1;
// 重寫module.exports
module.exports = { xxx: xxx };

/* 導入 */
 // 能夠訪問package.a / package.b...
const package = require('module-name');
// 結構賦值
const { a, b, c } = require('./uppercase.js');
複製代碼
原理分析

下面是兩個具備引用關係的模塊的打包文件,刪去各類花裏胡哨的註釋後,露出了很是簡單的真面目。服務器

源碼:

// index.js
const m = require('./module1.js');
console.log(m)

// module1.js
const m = 1;
module.exports = m;
複製代碼

打包後的文件以下:

能夠很清楚地看到,webpack把整個打包後的代碼處理成了一個當即執行的函數,參數是一個包含全部模塊的對象,每一個文件表現爲一個鍵值對,用文件路徑字符串做爲屬性名,將文件內的代碼,也就是整個模塊的內容全都包裝進了一個函數裏做爲屬性的值。在模塊內部使用的require也用__webpack_require__方法來替換了。

// 1. 是一個當即執行的函數
(function (modules) {
  var installedModules = {};
  
  // 4. 執行函數
  function __webpack_require__(moduleId) {
    // 5. 檢查若是有緩存直接返回
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 6. 建立一個模塊並存入緩存
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    // 7. 根據模塊id從模塊對象裏取出並執行
    // this綁定到module.exports 並注入module, module.exports
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // 8. 標記這個模塊爲已加載
    module.l = true;
    // 9. 返回module.exports
    return module.exports;
  }
  // 3. 傳入入口文件名
  return __webpack_require__(__webpack_require__.s = "./index.js");
})({ // 2. 將模塊對象做爲參數傳入
  "./index.js":
    (function (module, exports, __webpack_require__) {
      var m = __webpack_require__("./module1.js")
      console.log(m)
    }),

  "./module1.js":
    (function (module, exports) {
      var m = 1;
      module.exports = m;
    })
});
複製代碼

順着執行過程能夠發現CommonJS模塊處理的流程:

  1. 調用__webpack_require__,給要執行的模塊(文件)建立一個module。
var module = {
  i: moduleId,
  exports: {}
}
複製代碼
  1. 經過call方法調用這個模塊被包裝成的函數,將this綁定爲module.exports,傳入module和module.exports以及__webpack_require__方法。
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
複製代碼
  1. 若是這個模塊內部還有require別的模塊,會繼續調用__webpack_require__,遞歸重複這個過程。
  2. 一個模塊執行結束後返回module.exports。

因此咱們知道了,上面的使用示例中使用exports.xxx = xxxmodule.exports.xxx = xxx,其實本質上是同樣的,都是將變量掛到module.exports對象中,甚至還能夠寫成this.exports.xxx = xxx也有一樣的效果;別的模塊導入時,獲得的也是module.exports對象。

所以,若是直接使用module.exports = { xxx: xxx }的方式,就至關於重寫了導出的模塊,故而對 exportsthis.exports 的任何操做也就沒什麼意義了。

ES6 Module

概念

在ES6之前,JS沒有模塊體系。只有社區指定的一些模塊加載方案,如用於服務器端的CommonJS和用於瀏覽器端的AMD。

ES6導出的不是對象,沒法引用模塊自己,模塊的方法單獨加載。所以能夠在編譯時加載(也即靜態加載),於是能夠進行靜態分析,在編譯時就能夠肯定模塊的依賴關係和輸入輸出的變量,提高了效率。

而CommonJS和AMD輸出的是對象,引入時須要查找對象屬性,所以只能在運行時肯定模塊的依賴關係及輸入輸出變量(即運行時加載),所以沒法在編譯時作「靜態優化」。

使用示例以下:

/* 導出 */
// 導出一個變量
export let firstName = 'Michael';
// 導出多個變量
let firstName = 'Michael';
let lastName = 'Jackson';
export { firstName, lastName };
// 導出一個函數
export function multiply(x, y) { 
  return x * y;
}
// 給導出的變量重命名
export {
  v1 as streamV1,
  v2 as streamV2
};
// 默認輸出(本質上將輸出變量賦值給default),import時能夠隨便命名且無需加大括號{}:
export default function crc32() {}

/* 引用 */
// import只能放在文件頂層
import { stat, exists, readFild } from 'fs';
import { lastName as surname } from './profile.js'
// 引入整個模塊,而後用circle.xxx獲取內部變量或方法
import * as circle from './circle';
import crc from 'crc32';
複製代碼
原理分析

源碼:

// index.js
import { m } from './module1';
console.log(m);

// module1.js
const m = 1;
const n = 2;
export { m, n };
複製代碼

打包後:

// 1. 是一個當即執行函數
(function (modules) {
  var installedModules = {};
  // 4. 處理入口文件模塊
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 5. 建立一個模塊
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    // 6. 執行入口文件模塊函數
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    module.l = true;
    // 7. 返回
    return module.exports;
  }
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) { // 判斷name是否是exports本身的屬性
      Object.defineProperty(exports, name, {enumerable: true, get: getter});
    }
  };
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      // Symbol.toStringTag做爲對象的屬性,值表示這個對象的自定義類型 [Object Module]
      // 一般只做爲Object.prototype.toString()的返回值
      Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
    }
    Object.defineProperty(exports, '__esModule', {value: true});
  };
  __webpack_require__.o = function (object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
  };
  // 3. 傳入入口文件id
  return __webpack_require__(__webpack_require__.s = "./index.js");
})({ // 2. 模塊對象做爲參數傳入
  "./index.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      // __webpack_exports__就是module.exports
 "use strict";
      // 添加了__esModule和Symbol.toStringTag屬性
      __webpack_require__.r(__webpack_exports__);
      var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
      console.log(_module1__WEBPACK_IMPORTED_MODULE_0__["m"])
    }),

  "./module1.js":
    (function (module, __webpack_exports__, __webpack_require__) {
 "use strict";
      __webpack_require__.r(__webpack_exports__);
      // 把m/n這些變量添加到module.exports中,並設置getter爲直接返回值
      __webpack_require__.d(__webpack_exports__, "m", function () {return m;});
      __webpack_require__.d(__webpack_exports__, "n", function () {return n;});
      var m = 1;
      var n = 2;
    })
});
複製代碼

能夠看到,跟CommonJS差很少,一樣也是將模塊對象傳入一個當即執行的函數。只是模塊函數內部稍稍複雜了一些。執行一個模塊的流程前兩步和CommonJS同樣,也是先建立了一個module,而後再綁定this到module,傳入module和module.exports對象。

在模塊內部,導出模塊的流程是:

  1. 先給__webpack_exports__也就是module.exports對象添加一個Symbol.toStringTag屬性值爲{value: 'Module'},這麼作的做用就是使得module.exports調用toString方法能夠返回[Object Module]來代表這是一個模塊。
  2. 將要導出的變量添加到module.exports中,而後設置變量的getter,getter裏只是簡單地返回了同名變量的值。

導入的表現也很不同,不是單單導入module.exports這個對象,而是作了一些額外的工做。在這個例子中,index.js文件引入了m,而後打印了m。可是打包結果倒是導入的m和訪問的m並不相同,訪問m的時候其實訪問的是m['m'],也就是說webpack在訪問的時候自動幫咱們訪問內部的同名屬性。

import { m } from './module1';
console.log(m);

// webpack
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__["m"])
複製代碼

不一樣的導入和導出方式也會有不一樣的表現:

  1. 不一樣方式導出的表現:
  • 方式一:直接導出,只能是這三種形式,這三種是等價的。注意export後面不能接一個常量,由於export要輸出的是接口,要和變量一一對應,例如:var a = 1; export a;至關於 export 1 沒意義,會報錯。
export { bar, foo }
export var bar = xxx
export function foo = xxx 複製代碼
const obj = { a: 1 };
export { obj };

// webpack 
// __webpack_exports__就是導出的結果
__webpack_require__.d(__webpack_exports__, "obj", function() { return obj; });
var obj = { a: 1 };

// __webpack_require__.d這個函數首先判斷要導出的變量是否是__webpack_exports__上的屬性
// 若是不是就把這個變量掛在__webpack_exports__上,並設置getter
__webpack_require__.d = function(exports, name, getter) {
  if(!__webpack_require__.o(exports, name)) {
    Object.defineProperty(exports, name, { enumerable: true, get: getter });
  }
};
複製代碼
  • 方式二:export default的導出方式,只會簡單地把要導出的變量放在對象裏,而後掛到__webpack_exports__.default
let obj = { a: 1 }
export default { obj }

// webpack
var obj = { a: 1 };
__webpack_exports__["default"] = ({ obj: obj });

// export default後面跟什麼值就沒有限制了
export default obj
// webpack
__webpack_exports__["default"] = (obj);

export default obj.a
// webpack
__webpack_exports__["default"] = (obj.a);

export default 1
// webpack
__webpack_exports__["default"] = (1);
複製代碼
  1. 不一樣方式導入的表現:
  • 方式一:總體導入:直接獲取整個模塊的值__webpack_exports__,訪問的時候會自動查找default屬性的值,若是導出沒有使用export default,會獲得一個undefined。
import obj from './module1'
console.log(obj) // 沒有default會獲得undefined
obj.c = 2; // 沒有default會報錯

// webpack
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__["default"]);
_module1__WEBPACK_IMPORTED_MODULE_0__["default"].c = 2;
複製代碼
  • 方式二:以 * 總體導入,不會自動查找內層屬性,直接訪問__webpack_exports__
import * as obj from './module1'
console.log(obj)
obj.c = 2;

// webpack
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__);
_module1__WEBPACK_IMPORTED_MODULE_0__["c"] = 2;
複製代碼
  • 方式三:解構賦值的方式導入具體模塊,訪問的時候會自動查找花括號裏同名的屬性值。
import { obj } from './module1'
console.log(obj)
obj.c = 2;

// webpack
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module1.js");
console.log(_module1__WEBPACK_IMPORTED_MODULE_0__["obj"]);
_module1__WEBPACK_IMPORTED_MODULE_0__["obj"].c = 2;
複製代碼

CommonJS和ES6 Module的對比

如今就能明白文章開頭所說的動態加載、靜態加載、複製、引用都是什麼意思了。

CommonJS導出的是對象,內部要導出的變量在導出的那一刻就已經賦值給對象的屬性了,也就有了「CommonJS輸出的是值的拷貝」這種說法,後面再在模塊裏修改變量,其餘模塊是感受不到的,由於已經沒有關係了。可是對象仍是會影響,由於對象拷貝的只是對象的引用。

也是由於CommonJS導出的是對象,在編譯階段不會讀取對象的內容,並不清楚對象內部都導出了哪些變量、這些變量是否是從別的文件導入進來的。只有等到代碼運行時才能訪問對象的屬性,肯定依賴關係。所以才說CommonJS的模塊是動態加載的。

而對ES6 Module來講,因爲內部對每一個變量都定義了getter,所以其餘模塊導入後訪問變量時觸發getter,返回模塊裏的同名變量,若是變量值發生變化,則外邊的引用也會變化。

可是export default沒有走getter的形式,也是直接賦值,因此輸出的也是一份拷貝。例以下列代碼,能夠看到只是簡單地將變量 m 的值拷貝了一份掛到 default 屬性上。(經評論區提醒,webpack5 更正了這一行爲,default 也走 getter 了。)

const m = 1;
export default m;

// webpack
(function (module, __webpack_exports__, __webpack_require__) {
 "use strict";
  __webpack_require__.r(__webpack_exports__);
  var m = 1;
  __webpack_exports__["default"] = (m);
})
複製代碼

ES6 Module導出的不是一個對象,導出的是一個個接口,所以在編譯時就能肯定模塊之間的依賴關係,因此才說ES6 Module是靜態加載的。Tree Shaking就是根據這個特性在編譯階段搖掉無用模塊的。

ES6 Module還提供了一個import()方法動態加載模塊,返回一個Promise。

References

  1. es6.ruanyifeng.com/#docs/modul…
相關文章
相關標籤/搜索