commonjs 與 esm 的區別

js 社區存在多種模塊化規範,其中最常使用到的是 node 自己實現的 commonjs 和 es6 標準的 esm。node

commonjs 和 esm 存在多種根本上的區別,詳細的比較在阮一峯的《es6標準入門》已經寫得很詳細了,這裏我想用本身的思路從新總結一下。同時分析一下 babel 對於 esm 的編譯轉換,存在的侷限。webpack

commonjs 和 esm 的主要區別能夠歸納成如下幾點:es6

  1. 輸出拷貝 vs 輸出引用
  2. esm 的 import read-only 特性
  3. esm 存在 export/import 提高

下面對這三點作具體分析。web

輸出拷貝 vs 輸出引用

首先看個 commonjs 輸出拷貝的例子:segmentfault

// a.js
let a = 1;
let b = { num: 1 }
setTimeout(() => {
    a = 2;
    b = { num: 2 };
}, 200);
module.exports = {
    a,
    b,
};

// main.js
// node main.js
let {a, b} = require('./a');
console.log(a);  // 1
console.log(b);  // { num: 1 }
setTimeout(() => {
    console.log(a);  // 1
    console.log(b);  // { num: 1 }
}, 500);
複製代碼

所謂輸出拷貝,若是瞭解過 node 或者 webpack 對 commonjs 的實現(不瞭解能夠看我以前的文章),就會知道:exports 對象是模塊內外的惟一關聯, commonjs 輸出的內容,就是 exports 對象的屬性,模塊運行結束,屬性就肯定了。bash

再看 esm 輸出引用的例子:babel

// a.mjs
let a = 1;
let b = { num: 1 }
setTimeout(() => {
    a = 2;
    b = { num: 2 };
}, 200);
export {
    a,
    b,
};

// main.mjs
// node --experimental-modules main.mjs
import {a, b} from './a';
console.log(a);  // 1
console.log(b);  // { num: 1 }
setTimeout(() => {
    console.log(a);  // 2
    console.log(b);  // { num: 2 }
}, 500);
複製代碼

這就是 esm 輸出引用跟 commonjs 輸出值的區別,模塊內部引用的變化,會反應在外部,這是 esm 的規範。模塊化

esm 的 import read-only 特性

read-only 的特性很好理解,import 的屬性是隻讀的,不能賦值,相似於 const 的特性,這裏就不舉例解釋了。ui

esm 存在 export/import 提高

esm 對於 import/export 存在提高的特性,具體表現是規範規定 import/export 必須位於模塊頂級,不能位於做用域內;其次對於模塊內的 import/export 會提高到模塊頂部,這是在編譯階段完成的。spa

esm 的 import/export 提高在正常狀況下,使用起來跟 commonjs 沒有區別,由於通常狀況下,咱們在引入模塊的時候,都會在模塊的同步代碼執行完才獲取到輸出值。因此即便存在提高,也沒法感知。

因此要想驗證這個事實,須要考慮到循環依賴的狀況。循環依賴指的是模塊A依賴模塊B,模塊B又依賴模塊A,互相依賴產生了死循環。因此各個模塊方案自己設計了一套規則來解決這個問題。在循環依賴的狀況下,模塊會出現執行中斷,而後咱們能夠看到 import/export 提高和 commonjs 的區別。

這裏用2個循環依賴的例子來解釋,首先看 commonjs 的表現:

// a.js
exports.done = false;
let b = require('./b');
console.log('a.js: b.done = %j', b.done);  // true
exports.done = true;
console.log('a.js執行完畢');

// b.js
exports.done = false;
let a = require('./a');
console.log('b.js: a.done = %j', a.done);  // false
exports.done = true;
console.log('b.js執行完畢');

// main.js
let a = require('./a');
let b = require('./b');
console.log('main.js: a.done = %j, b.done = %j', a.done, b.done);  // true true

// 輸出結果
// node main.js
b.js: a.done = false
b.js執行完畢
a.js: b.done = true
a.js執行完畢
main.js: a.done = true, b.done = true
複製代碼

這是《es6入門》裏的循環依賴的例子,這個例子能提現 commonjs 運行時加載的狀況。由於 a.js 依賴 b.js,b.js 又依賴 a.js,因此當 b.js 執行到require('./a')的時候,a.js 會暫停執行,因此此時require('./a')返回的是false,可是在main.js中,a.js的返回值又是true,因此這說明了 commonjs 模塊的 exports 是動態執行的,具體 require 能獲取到的值,取決於模塊的運行狀況。

下面是 esm 的循環依賴的例子:

// a.mjs
export let a_done = false;
import { b_done } from './b';
console.log('a.js: b.done = %j', b_done);
console.log('a.js執行完畢');

// b.mjs
import { a_done } from './a';
console.log('b.js: a.done = %j', a_done);
export let b_done = true;
console.log('b.js執行完畢');

// main.mjs
import { a_done } from './a';
import { b_done } from './b';
console.log('main.js: a.done = %j, b.done = %j', a_done, b_done);

// 輸出結果
// node --experimental-modules main.mjs
ReferenceError: a_done is not defined
複製代碼

這裏解釋一下,爲何a_done is not defined。a.mjs 加載 b.mjs,而 b.mjs 又加載 a.mjs,這就造成了循環依賴。循環依賴產生時,a.mjs 中斷執行,這時在 b.mjs 中a_done的值是什麼呢?這就要考慮到 a.mjs 的 import/export 提高的問題,a.mjs 中的export a_done被提高到頂部,執行到import './b'時,執行權限移交到 b.mjs,此時a_done只是一個指定導出的接口,可是未定義,因此出現引用報錯。

這裏先提一下,若是用 babel 來編譯執行,是不會報錯的,執行結果以下:

// npx babel-node src/main.mjs
b.js: a.done = undefined
b.js執行完畢
a.js: b.done = true
a.js執行完畢
main.js: a.done = false, b.done = true
複製代碼

爲何呢?後面會來分析。

bebel 模擬 esm

這一節來看看,babel 是怎麼實現 esm 這幾個特性的:輸出引用、read-only。

仍是上面的例子,稍微改一下:

// a.js
let a = 1;
let b = { num: 1 }
setTimeout(() => {
    a = 2;
    b = { num: 2 };
}, 200);
export {
    a,
    b,
};

// main.js
import {a, b} from './a';
console.log(a);
console.log(b);
setTimeout(() => {
    console.log(a);
    console.log(b);
}, 500);

a = 3;
複製代碼

用babel編譯一下,生成了以下的內容:

// a.js
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.b = exports.a = void 0;
var a = 1;
exports.a = a;
var b = {
  num: 1
};
exports.b = b;
setTimeout(function () {
  exports.a = a = 2;
  exports.b = b = {
    num: 2
  };
}, 200);

// main.js
"use strict";
var _a = require("./a");
console.log(_a.a);
console.log(_a.b);
setTimeout(function () {
  console.log(_a.a);
  console.log(_a.b);
}, 500);
_a.a = (3, function () {
  throw new Error('"' + "a" + '" is read-only.');
}());
複製代碼

簡單分析一下,對於輸出引用,babel 是經過在輸出屬性變化時,同步修改 exports 對象對應的屬性來實現的,好比像這樣的代碼:

exports.a = a = 2;

另一個特性 read-only,babel 經過拋異常的方式來實現,好比這樣的代碼:

_a.a = (3, function () {
  throw new Error('"' + "a" + '" is read-only.');
}());
複製代碼

bebel 模擬 esm 的侷限

前面關於 esm 的 import/export 提高的例子,在 node 原生 esm 環境下和babel 編譯環境下的執行結果不一致,這是什麼緣由呢?咱們把前面的例子用 babel 編譯一下,看看轉換成什麼形式的代碼。

首先仍是貼一下 esm 代碼:

// a.mjs
export let a_done = false;
import { b_done } from './b';
console.log('a.js: b.done = %j', b_done);
console.log('a.js執行完畢');

// b.mjs
import { a_done } from './a';
console.log('b.js: a.done = %j', a_done);
export let b_done = true;
console.log('b.js執行完畢');

// main.mjs
import { a_done } from './a';
import { b_done } from './b';
console.log('main.js: a.done = %j, b.done = %j', a_done, b_done);
複製代碼

用 babel 編譯一下,生成了以下的內容:

// a.js
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.a_done = void 0;
var _b = require("./b");
var a_done = false;
exports.a_done = a_done;
console.log('a.js: b.done=%j', _b.b_done);
console.log('a.js執行完畢');

// b.js
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.b_done = void 0;
var _a = require("./a");
console.log('b.js: a.done=%j', _a.a_done);
var b_done = true;
exports.b_done = b_done;
console.log('b.js執行完畢');

// main.js
"use strict";
var _a = require("./a");
var _b = require("./b");
console.log('main.js: a.done=%j, b.done=%j', _a.a_done, _b.b_done);
複製代碼

能夠看到,babel 也實現了 export 的提高,輸出值統一設置爲void 0,可是想象一下,a_done實際上是 export 對象的屬相,那麼在 commonjs 的環境下,從對象取值,只可能會出現undefined,而不可能出現is not defined

其實根本緣由也是源於 commonjs 輸出的是對象,而 esm 輸出的是引用,babel 本質是利用 commonjs 來模擬 esm,因此這個特性也是 babel 沒法模擬實現的。

結論

本文主要總結了 commonjs 跟 esm 的主要對比,而且分析了 babel 模擬 esm 的方式和侷限。

文章主要是我的的理解和總結,若有錯誤歡迎指正。

相關文章
相關標籤/搜索