關于JS模塊化時循環(huán)加載的那些事兒
關于JS模塊化的相關知識有很多,一些基本的知識平時都會有注意到藕帜,但是在實踐中發(fā)現(xiàn)對于模塊循環(huán)加載的情景不太注意,本文主要記錄一下在webpack中使用模塊化的一些心得票渠。
說到模塊化拂檩,離不開CommonJS和ES6,有的項目在使用時也會同時存在這兩種語法啊送,至于AMD和CMD不在本文討論范圍內(nèi)偿短。
CommonJS中的循環(huán)加載
來看一個簡單的例子,定義了四個文件a.js馋没、b.js、main.js降传、TestImportExport.js篷朵,其中a.js和b.js相互引用,形成一個循環(huán)引用婆排,main.js中引用a.js和b.js声旺,TestImportExport.js用于測試。
1段只、a.js
//a.js
const b = require('./b.js');
console.log('a.js中的b:',b);
const sayHi = (name) => {
console.log('Hi,',name);
}
const callFunc=(name)=>{
b.sayBye(name);
}
module.exports = {
sayHi: sayHi,
callFunc:callFunc
}
2腮猖、b.js
//b.js
const a = require('./a.js');
console.log('b.js中的a:',a);
const sayBye = (name)=>{
console.log('Bye,',name);
}
const callFunc=(name)=>{
a.sayHi(name);
}
module.exports={
sayBye:sayBye,
callFunc:callFunc
}
3、main.js
//main.js
const a = require('./a.js');
const b = require('./b.js');
const f = () => {
a.sayHi('zzx');
b.sayBye('zzx');
a.callFunc('zzx');
b.callFunc('zzx');
}
export { f }
4赞枕、TestImportExport.js
//TestImportExport.js
import React from 'react';
import * as testmodule from './test_import_export/main';
function TestImportExport() {
console.log(testmodule);
return (
<div>
<button onClick={ testmodule.f}>測試</button>
</div>
);
}
export default TestImportExport;
OK,現(xiàn)在問題來了澈缺,我在瀏覽器中打開test頁面,日志臺會輸出什么結(jié)果炕婶?點擊‘測試’按鈕之后又會輸出什么結(jié)果姐赡?
直接上圖
打開時:
從圖中可以看到,打開時console的輸出順序是:b.js-a.js-TestImportExport.js,為什么呢?
原因是webpack會先執(zhí)行import或者require的模塊柠掂。所以執(zhí)行順序是TestImportExport先去運行main项滑,運行main時發(fā)現(xiàn)main引用了a,又去運行a,運行a時發(fā)現(xiàn)a引用了b,又去運行b涯贞。此時b又引用了a枪狂,但是剛剛已經(jīng)運行過a了危喉,所以b會接著往下執(zhí)行,輸出第一個console州疾,此時由于a只執(zhí)行了部分所以此時引入的a是一個空對象姥饰。執(zhí)行完b之后繼續(xù)執(zhí)行a,輸出b。執(zhí)行完a之后繼續(xù)執(zhí)行main孝治,此時不會重復去加載b列粪,而是使用在a中已經(jīng)加載好的b。最后輸出TestImportExport中的console谈飒。
點擊按鈕后:
點擊按鈕后回去執(zhí)行f中的函數(shù)岂座,a.sayHi、b.sayBye杭措、a.callFunc都能正常運行费什,b.callFunc會報錯,因為b中的a在執(zhí)行時就確定了是一個空對象手素,所以無法獲取相應的函數(shù),執(zhí)行出錯鸳址。
現(xiàn)在,我們來看一看另外一種寫法的CommonJS,修改一下a.js文件,其余文件不變
//a.js
const b = require('./b.js');
console.log('a.js中的b:',b);
const sayHi = (name) => {
console.log('Hi,',name);
}
const callFunc=(name)=>{
b.sayBye(name);
}
// module.exports = {
// sayHi: sayHi,
// callFunc:callFunc
// }
exports.sayHi=sayHi;
exports.callFunc=callFunc;
OK,再來看一看現(xiàn)在的結(jié)果
打開時:
點擊按鈕后:
從圖中可以看到泉懦,在b.js中加載后立即使用的a仍然是一個空對象稿黍,因為此時a還沒有執(zhí)行完;在點擊按鈕后函數(shù)調(diào)用時又能正常運行崩哩,原因是exports是module.exports的一個引用巡球,即使已經(jīng)緩存了a這個模塊,但是在執(zhí)行完b.js繼續(xù)執(zhí)行a.js時通過exports仍然可以對a模塊的module.exports進行修改邓嘹,而實際函數(shù)調(diào)用是在執(zhí)行完a.js之后酣栈,所以實際函數(shù)調(diào)用時是有值的,在執(zhí)行完a.js之前是無值的汹押。
引用阮一峰老師在ES6入門一書中所寫的內(nèi)容:
總之矿筝,CommonJS 輸入的是被輸出值的拷貝,不是引用棚贾。
另外窖维,由于 CommonJS 模塊遇到循環(huán)加載時,返回的是當前已經(jīng)執(zhí)行的部分的值鸟悴,而不是代碼全部執(zhí)行后的值陈辱,兩者可能會有差異。所以细诸,輸入變量的時候沛贪,必須非常小心。
ES6中的循環(huán)加載
來自阮一峰老師ES6入門一書對ES6處理‘循環(huán)加載’的描述
ES6 處理“循環(huán)加載”與 CommonJS 有本質(zhì)的不同。ES6 模塊是動態(tài)引用利赋,如果使用import從一個模塊加載變量(即import foo from 'foo')水评,那些變量不會被緩存,而是成為一個指向被加載模塊的引用媚送,需要開發(fā)者自己保證中燥,真正取值的時候能夠取到值。
來看一個簡單的例子塘偎,定義了四個文件a.js疗涉、b.js、main.js吟秩、TestImportExport.js咱扣,其中a.js和b.js相互引用,形成一個循環(huán)引用涵防,main.js中引用a.js和b.js闹伪,TestImportExport.js用于測試。
1壮池、a.js
//a.js es6
import * as b from './b'
console.log('a.js中的b:', b);
const sayHi = (name) => {
console.log('Hi,', name);
}
const callFunc = (name) => {
b.sayBye(name);
}
export {
sayHi,
callFunc
}
2偏瓤、b.js
//b.js es6
import * as a from './a'
console.log('b.js中的a:', a);
const sayBye = (name) => {
console.log('Bye,', name);
}
const callFunc = (name) => {
a.sayHi(name);
}
export {
sayBye,
callFunc
}
3、main.js
//main.js es6
import * as a from './a.js'
import * as b from './b.js'
const f = () => {
a.sayHi('zzx');
b.sayBye('zzx');
a.callFunc('zzx');
b.callFunc('zzx');
}
export { f }
4椰憋、TestImportExport.js
//TestImportExport.js es6
import React from 'react';
import * as testmodule from './test_ES6/main';
function TestImportExport() {
console.log(testmodule);
return (
<div>
<button onClick={ testmodule.f}>測試</button>
</div>
);
}
export default TestImportExport;
OK,再來看一看ES6循環(huán)加載時控制臺會輸出什么厅克,直接上圖:
打開頁面時:
從圖中看到,ES6的import加載在webpack中會被一個視作Module熏矿,建立一個引用鏈接已骇,在真正使用時才會鏈接地址取值
點擊按鈕后:
可以看到即使出現(xiàn)了循環(huán)加載ES6仍然能夠正確運行。原因就是webpack會在真正使用模塊中的內(nèi)容時才會去鏈接地址取值票编。
ES6循環(huán)加載就不會出錯嗎?
不是的卵渴,雖然大多數(shù)情況下ES6中即使循環(huán)加載了也能正確處理慧域,但是也有一些特殊情況下會出錯,比如在import后立即調(diào)用import的值浪读,而不是寫在函數(shù)中昔榴。
一個簡單的例子,將上面ES6中的a.js和b.js做簡單的修改:
//a.js es6
import * as b from './b'
console.log('a.js中的b:', b);
b.sayBye('zzx');//在import之后立即調(diào)用
const sayHi = (name) => {
console.log('Hi,', name);
}
const callFunc = (name) => {
b.sayBye(name);
}
export {
sayHi,
callFunc
}
//b.js es6
import * as a from './a'
console.log('b.js中的a:', a);
a.sayHi('zzx');//在import之后立即調(diào)用
const sayBye = (name) => {
console.log('Bye,', name);
}
const callFunc = (name) => {
a.sayHi(name);
}
export {
sayBye,
callFunc
}
打開網(wǎng)頁時控制臺輸出如下:
從圖中可以看到,在b中引入a之后立即調(diào)用a中的內(nèi)容是會出錯的碘橘,因為此時還未完成整個引用鏈路的鏈接互订。
綜上,在ES6語法中出現(xiàn)循環(huán)加載是能夠正確處理的痘拆,但是不能在import之后立即使用加載的模塊仰禽,盡量把所有操作都寫在函數(shù)中。
但是從代碼質(zhì)量上來說我們在書寫代碼時應該避免這種循環(huán)加載的情形,需要仔細去考慮代碼的拆分吐葵。
本期介紹暫告一段落规揪,下期介紹CommonJS和ES6語法共用的情形。
參考鏈接:
關于CommonJS語法參考:https://javascript.ruanyifeng.com/nodejs/module.html#toc5
關于exports和module.exports參考:https://juejin.im/post/597ec55a51882556a234fcef