CommonJS和ES6模組迴圈載入處理的區別

鄒R-ainna發表於2018-12-26

CommonJS模組規範使用require語句匯入模組,module.exports匯出模組,輸出的是值的拷貝,模組匯入的也是輸出值的拷貝,也就是說,一旦輸出這個值,這個值在模組內部的變化是監聽不到的。

ES6模組的規範是使用import語句匯入模組,export語句匯出模組,輸出的是對值的引用。ES6模組的執行機制和CommonJS不一樣,遇到模組載入命令import時不去執行這個模組,只會生成一個動態的只讀引用,等真的需要用到這個值時,再到模組中取值,也就是說原始值變了,那輸入值也會發生變化。

那CommonJS和ES6模組規範針對模組的迴圈載入處理機制有什麼不同呢?

迴圈載入指的是a指令碼的執行依賴b指令碼,b指令碼的執行依賴a指令碼。

1. CommonJS模組的載入原理

CommonJS模組就是一個指令碼檔案,require命令第一次載入該指令碼時就會執行整個指令碼,然後在記憶體中生成該模組的一個說明物件。

{
    id: '',  //模組名,唯一
    exports: {  //模組輸出的各個介面
        ...
    },
    loaded: true,  //模組的指令碼是否執行完畢
    ...
}
複製程式碼

以後用到這個模組時,就會到物件的exports屬性中取值。即使再次執行require命令,也不會再次執行該模組,而是到快取中取值。

CommonJS模組是載入時執行,即指令碼程式碼在require時就全部執行。一旦出現某個模組被“迴圈載入”,就只輸出已經執行的部分,沒有執行的部分不會輸出。

案例說明:

案例來源於Node官方說明:nodejs.org/api/modules…

//a.js
exports.done = false;

var b = require('./b.js');
console.log('在a.js中,b.done = %j', b.done);

exports.done = true;
console.log('a.js執行完畢!')
複製程式碼
//b.js
exports.done = false;

var a = require('./a.js');
console.log('在b.js中,a.done = %j', a.done);

exports.done = true;
console.log('b.js執行完畢!')
複製程式碼
//main.js
var a = require('./a.js');
var b = require('./b.js');

console.log('在main.js中,a.done = %j, b.done = %j', a.done, b.done);
複製程式碼

輸出結果如下:

//node環境下執行main.js
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
複製程式碼

JS程式碼執行順序如下:

1)main.js中先載入a.js,a指令碼先輸出done變數,值為false,然後載入b指令碼,a的程式碼停止執行,等待b指令碼執行完成後,才會繼續往下執行。

2)b.js執行到第二行會去載入a.js,這時發生迴圈載入,系統會去a.js模組對應物件的exports屬性取值,因為a.js沒執行完,從exports屬性只能取回已經執行的部分,未執行的部分不返回,所以取回的值並不是最後的值。

3)a.js已執行的程式碼只有一行,exports.done = false;所以對於b.js來說,require a.js只輸出了一個變數done,值為false。往下執行console.log('在b.js中,a.done = %j', a.done);控制檯列印出:

在b.js中,a.done = false
複製程式碼

4)b.js繼續往下執行,done變數設定為true,console.log('b.js執行完畢!'),等到全部執行完畢,將執行權交還給a.js。此時控制檯輸出:

b.js執行完畢!
複製程式碼

5)執行權交給a.js後,a.js接著往下執行,執行console.log('在a.js中,b.done = %j', b.done);控制檯列印出:

在a.js中,b.done = true
複製程式碼

6)a.js繼續執行,變數done設定為true,直到a.js執行完畢。

a.js執行完畢!
複製程式碼

7)main.js中第二行不會再次執行b.js,直接輸出快取結果。最後控制檯輸出:

在main.js中,a.done = true, b.done = true
複製程式碼

總結:

1)在b.js中,a.js沒有執行完畢,只執行了第一行,所以迴圈載入中,只輸出已執行的部分。

2)main.js第二行不會再次執行,而是輸出快取b.js的執行結果。exports.done = true;

2. ES6模組的迴圈載入

ES6模組與CommonJS有本質區別,ES6模組對匯出變數,方法,物件是動態引用,遇到模組載入命令import時不會去執行模組,只是生成一個指向被載入模組的引用,需要開發者保證真正取值時能夠取到值,只要引用是存在的,程式碼就能執行。

案例說明:

//even.js
import {odd} from './odd';

var counter = 0;
export function even(n){
    counter ++;
    console.log(counter);
    
    return n == 0 || odd(n-1);
}
複製程式碼
//odd.js
import {even} from './even.js';

export function odd(n){
    return n != 0 && even(n-1);
}
複製程式碼
//index.js
import * as m from './even.js';

var x = m.even(5);
console.log(x);

var y = m.even(4);
console.log(y);
複製程式碼

執行index.js,輸出結果如下:

babel-node index.js

1
2
3
false
4
5
6
true
複製程式碼

可以看出counter的值是累加的,ES6是動態引用。如果上面的引用改為CommonJS程式碼,會報錯,因為在odd.js裡,even.js程式碼並沒有執行。改成CommonJS規範載入的程式碼為:

//even.js
var odd = require('./odd.js');

var counter = 0;
module.exports = function even(n){
    counter ++;
    console.log(counter);

    return n == 0 || odd(n-1);
}
複製程式碼
//odd.js
var even = require('./even.js');

module.exports = function odd(n){
    return n != 0 && even(n-1);
}
複製程式碼
//index.js
var even = require('./even.js');

var x = even(5);
console.log(x);

var y = even(5);
console.log(y);
複製程式碼

執行index.js,輸出結果如下:

$ babel-node index.js
1
/Users/name/Projects/node/ES6/odd.1.js:6
    return n != 0 && even(n - 1);
                     ^

TypeError: even is not a function
    at odd (/Users/name/Projects/node/ES6/odd.1.js:4:22)
複製程式碼

3. 總結

1)CommonJS模組是載入時執行。一旦出現某個模組被“迴圈載入”,就只輸出已經執行的部分,沒有執行的部分不會輸出。

2)ES6模組對匯出模組,變數,物件是動態引用,遇到模組載入命令import時不會去執行模組,只是生成一個指向被載入模組的引用。

CommonJS模組規範主要適用於後端Node.js,後端Node.js是同步模組載入,所以在模組迴圈引入時模組已經執行完畢。推薦前端工程中使用ES6的模組規範,通過安裝Babel轉碼外掛支援ES6模組引入的語法。

頁面內容主要來源於《ES6標準入門》Module 這一章的介紹。如果有描述不清楚或錯誤的地方,歡迎留言指證。

參考資料:

《ES6標準入門》之Module

Node.js Cycle

ES-Module-Loader

相關文章