CommonJS和ES6模塊循環加載處理的區別

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

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

那CommonJS和ES6模塊規範針對模塊的循環加載處理機制有什麼不一樣呢?

循環加載指的是a腳本的執行依賴b腳本,b腳本的執行依賴a腳本。node

1. CommonJS模塊的加載原理

CommonJS模塊就是一個腳本文件,require命令第一次加載該腳本時就會執行整個腳本,而後在內存中生成該模塊的一個說明對象。git

{
    id: '',  //模塊名,惟一
    exports: {  //模塊輸出的各個接口
        ...
    },
    loaded: true,  //模塊的腳本是否執行完畢
    ...
}
複製代碼

之後用到這個模塊時,就會到對象的exports屬性中取值。即便再次執行require命令,也不會再次執行該模塊,而是到緩存中取值。github

CommonJS模塊是加載時執行,即腳本代碼在require時就所有執行。一旦出現某個模塊被「循環加載」,就只輸出已經執行的部分,沒有執行的部分不會輸出。後端

案例說明:api

案例來源於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);
複製代碼

輸出結果以下:bash

//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代碼執行順序以下:babel

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

相關文章
相關標籤/搜索