webpack核心模塊tapable源碼解析
上一篇文章我寫了tapable的基本用法,我們知道他是一個(gè)增強(qiáng)版版的發(fā)布訂閱模式,本文想來(lái)學(xué)習(xí)下他的源碼。tapable的源碼我讀了一下,發(fā)現(xiàn)他的抽象程度比較高,直接扎進(jìn)去反而會(huì)讓人云里霧里的,所以本文會(huì)從最簡(jiǎn)單的SyncHook和發(fā)布訂閱模式入手,再一步一步抽象,慢慢變成他源碼的樣子。
本文可運(yùn)行示例代碼已經(jīng)上傳GitHub,大家拿下來(lái)一邊玩一邊看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-source-code。
SyncHook的基本實(shí)現(xiàn)
上一篇文章已經(jīng)講過SyncHook的用法了,我這里就不再展開了,他使用的例子就是這樣子:
const?{?SyncHook?}?=?require("tapable");
//?實(shí)例化一個(gè)加速的hook
const?accelerate?=?new?SyncHook(["newSpeed"]);
//?注冊(cè)第一個(gè)回調(diào),加速時(shí)記錄下當(dāng)前速度
accelerate.tap("LoggerPlugin",?(newSpeed)?=>
??console.log("LoggerPlugin",?`加速到${newSpeed}`)
);
//?再注冊(cè)一個(gè)回調(diào),用來(lái)檢測(cè)是否超速
accelerate.tap("OverspeedPlugin",?(newSpeed)?=>?{
??if?(newSpeed?>?120)?{
????console.log("OverspeedPlugin",?"您已超速!!");
??}
});
//?觸發(fā)一下加速事件,看看效果吧
accelerate.call(500);
其實(shí)這種用法就是一個(gè)最基本的發(fā)布訂閱模式,我之前講發(fā)布訂閱模式的文章講過,我們可以仿照那個(gè)很快實(shí)現(xiàn)一個(gè)SyncHook:
class?SyncHook?{
????constructor(args?=?[])?{
????????this._args?=?args;???????//?接收的參數(shù)存下來(lái)
????????this.taps?=?[];??????????//?一個(gè)存回調(diào)的數(shù)組
????}
????//?tap實(shí)例方法用來(lái)注冊(cè)回調(diào)
????tap(name,?fn)?{
????????//?邏輯很簡(jiǎn)單,直接保存下傳入的回調(diào)參數(shù)就行
????????this.taps.push(fn);
????}
????//?call實(shí)例方法用來(lái)觸發(fā)事件,執(zhí)行所有回調(diào)
????call(...args)?{
????????//?邏輯也很簡(jiǎn)單,將注冊(cè)的回調(diào)一個(gè)一個(gè)拿出來(lái)執(zhí)行就行
????????const?tapsLength?=?this.taps.length;
????????for(let?i?=?0;?i?????????????const?fn?=?this.taps[i];
????????????fn(...args);
????????}
????}
}
這段代碼非常簡(jiǎn)單,是一個(gè)最基礎(chǔ)的發(fā)布訂閱模式,使用方法跟上面是一樣的,將SyncHook從tapable導(dǎo)出改為使用我們自己的:
//?const?{?SyncHook?}?=?require("tapable");
const?{?SyncHook?}?=?require("./SyncHook");
運(yùn)行效果是一樣的:
image-20210323153234354注意: 我們構(gòu)造函數(shù)里面?zhèn)魅氲?code style="font-size:14px;color:rgb(30,107,184);background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">args并沒有用上,tapable主要是用它來(lái)動(dòng)態(tài)生成call的函數(shù)體的,在后面講代碼工廠的時(shí)候會(huì)看到。
SyncBailHook的基本實(shí)現(xiàn)
再來(lái)一個(gè)SyncBailHook的基本實(shí)現(xiàn)吧,SyncBailHook的作用是當(dāng)前一個(gè)回調(diào)返回不為undefined的值的時(shí)候,阻止后面的回調(diào)執(zhí)行。基本使用是這樣的:
const?{?SyncBailHook?}?=?require("tapable");????//?使用的是SyncBailHook
const?accelerate?=?new?SyncBailHook(["newSpeed"]);
accelerate.tap("LoggerPlugin",?(newSpeed)?=>
??console.log("LoggerPlugin",?`加速到${newSpeed}`)
);
//?再注冊(cè)一個(gè)回調(diào),用來(lái)檢測(cè)是否超速
//?如果超速就返回一個(gè)錯(cuò)誤
accelerate.tap("OverspeedPlugin",?(newSpeed)?=>?{
??if?(newSpeed?>?120)?{
????console.log("OverspeedPlugin",?"您已超速!!");
????return?new?Error('您已超速!!');
??}
});
//?由于上一個(gè)回調(diào)返回了一個(gè)不為undefined的值
//?這個(gè)回調(diào)不會(huì)再運(yùn)行了
accelerate.tap("DamagePlugin",?(newSpeed)?=>?{
??if?(newSpeed?>?300)?{
????console.log("DamagePlugin",?"速度實(shí)在太快,車子快散架了。。。");
??}
});
accelerate.call(500);
他的實(shí)現(xiàn)跟上面的SyncHook也非常像,只是call在執(zhí)行的時(shí)候不一樣而已,SyncBailHook需要檢測(cè)每個(gè)回調(diào)的返回值,如果不為undefined就終止執(zhí)行后面的回調(diào),所以代碼實(shí)現(xiàn)如下:
class?SyncBailHook?{
????constructor(args?=?[])?{
????????this._args?=?args;???????
????????this.taps?=?[];??????????
????}
????tap(name,?fn)?{
????????this.taps.push(fn);
????}
????//?其他代碼跟SyncHook是一樣的,就是call的實(shí)現(xiàn)不一樣
????//?需要檢測(cè)每個(gè)返回值,如果不為undefined就終止執(zhí)行
????call(...args)?{
????????const?tapsLength?=?this.taps.length;
????????for(let?i?=?0;?i?????????????const?fn?=?this.taps[i];
????????????const?res?=?fn(...args);
????????????if(?res?!==?undefined)?return?res;
????????}
????}
}
然后改下SyncBailHook從我們自己的引入就行:
//?const?{?SyncBailHook?}?=?require("tapable");?
const?{?SyncBailHook?}?=?require("./SyncBailHook");?
運(yùn)行效果是一樣的:
image-20210323155857678抽象重復(fù)代碼
現(xiàn)在我們只實(shí)現(xiàn)了SyncHook和SyncBailHook兩個(gè)Hook而已,上一篇講用法的文章里面總共有9個(gè)Hook,如果每個(gè)Hook都像前面這樣實(shí)現(xiàn)也是可以的。但是我們?cè)僮屑?xì)看下SyncHook和SyncBailHook兩個(gè)類的代碼,發(fā)現(xiàn)他們除了call的實(shí)現(xiàn)不一樣,其他代碼一模一樣,所以作為一個(gè)有追求的工程師,我們可以把這部分重復(fù)的代碼提出來(lái)作為一個(gè)基類:Hook類。
Hook類需要包含一些公共的代碼,call這種不一樣的部分由各個(gè)子類自己實(shí)現(xiàn)。所以Hook類就長(zhǎng)這樣:
const?CALL_DELEGATE?=?function(...args)?{
?this.call?=?this._createCall();
?return?this.call(...args);
};
//?Hook是SyncHook和SyncBailHook的基類
//?大體結(jié)構(gòu)是一樣的,不一樣的地方是call
//?不同子類的call是不一樣的
//?tapable的Hook基類提供了一個(gè)抽象接口compile來(lái)動(dòng)態(tài)生成call函數(shù)
class?Hook?{
????constructor(args?=?[])?{
????????this._args?=?args;???????
????????this.taps?=?[];??????????
????????//?基類的call初始化為CALL_DELEGATE
????????//?為什么這里需要這樣一個(gè)代理,而不是直接this.call?=?_createCall()
????????//?等我們后面子類實(shí)現(xiàn)了再一起講
????????this.call?=?CALL_DELEGATE;
????}
????//?一個(gè)抽象接口compile
????//?由子類實(shí)現(xiàn),基類compile不能直接調(diào)用
????compile(options)?{
??????throw?new?Error("Abstract:?should?be?overridden");
????}
????tap(name,?fn)?{
????????this.taps.push(fn);
????}
????//?_createCall調(diào)用子類實(shí)現(xiàn)的compile來(lái)生成call方法
????_createCall()?{
??????return?this.compile({
????????taps:?this.taps,
????????args:?this._args,
??????});
?}
}
官方對(duì)應(yīng)的源碼看這里:https://github.com/webpack/tapable/blob/master/lib/Hook.js
子類SyncHook實(shí)現(xiàn)
現(xiàn)在有了Hook基類,我們的SyncHook就需要繼承這個(gè)基類重寫,tapable在這里繼承的時(shí)候并沒有使用class extends,而是手動(dòng)繼承的:
const?Hook?=?require('./Hook');
function?SyncHook(args?=?[])?{
????//?先手動(dòng)繼承Hook
???const?hook?=?new?Hook(args);
????hook.constructor?=?SyncHook;
????//?然后實(shí)現(xiàn)自己的compile函數(shù)
????//?compile的作用應(yīng)該是創(chuàng)建一個(gè)call函數(shù)并返回
??hook.compile?=?function(options)?{
????????//?這里call函數(shù)的實(shí)現(xiàn)跟前面實(shí)現(xiàn)是一樣的
????????const?{?taps?}?=?options;
????????const?call?=?function(...args)?{
????????????const?tapsLength?=?taps.length;
????????????for(let?i?=?0;?i?????????????????const?fn?=?this.taps[i];
????????????????fn(...args);
????????????}
????????}
????????return?call;
????};
????
?return?hook;
}
SyncHook.prototype?=?null;
注意:我們?cè)诨?code style="font-size:14px;color:rgb(30,107,184);background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">Hook構(gòu)造函數(shù)中初始化this.call為CALL_DELEGATE這個(gè)函數(shù),這是有原因的,最主要的原因是確保this的正確指向。思考一下假如我們不用CALL_DELEGATE,而是直接this.call = this._createCall()會(huì)發(fā)生什么?我們來(lái)分析下這個(gè)執(zhí)行流程:
- 用戶使用時(shí),肯定是使用
new SyncHook(),這時(shí)候會(huì)執(zhí)行const hook = new Hook(args); new Hook(args)會(huì)去執(zhí)行Hook的構(gòu)造函數(shù),也就是會(huì)運(yùn)行this.call = this._createCall()- 這時(shí)候的
this指向的是基類Hook的實(shí)例,this._createCall()會(huì)調(diào)用基類的this.compile() - 由于基類的
complie函數(shù)是一個(gè)抽象接口,直接調(diào)用會(huì)報(bào)錯(cuò)Abstract: should be overridden。
那我們采用this.call = CALL_DELEGATE是怎么解決這個(gè)問題的呢?
- 采用
this.call = CALL_DELEGATE后,基類Hook上的call就只是被賦值為一個(gè)代理函數(shù)而已,這個(gè)函數(shù)不會(huì)立馬調(diào)用。 - 用戶使用時(shí),同樣是
new SyncHook(),里面會(huì)執(zhí)行Hook的構(gòu)造函數(shù) Hook構(gòu)造函數(shù)會(huì)給this.call賦值為CALL_DELEGATE,但是不會(huì)立即執(zhí)行。new SyncHook()繼續(xù)執(zhí)行,新建的實(shí)例上的方法hook.complie被覆寫為正確方法。- 當(dāng)用戶調(diào)用
hook.call的時(shí)候才會(huì)真正執(zhí)行this._createCall(),這里面會(huì)去調(diào)用this.complie() - 這時(shí)候調(diào)用的
complie已經(jīng)是被正確覆寫過的了,所以得到正確的結(jié)果。
子類SyncBailHook的實(shí)現(xiàn)
子類SyncBailHook的實(shí)現(xiàn)跟上面SyncHook的也是非常像,只是hook.compile實(shí)現(xiàn)不一樣而已:
const?Hook?=?require('./Hook');
function?SyncBailHook(args?=?[])?{
????//?基本結(jié)構(gòu)跟SyncHook都是一樣的
???const?hook?=?new?Hook(args);
????hook.constructor?=?SyncBailHook;
????
????//?只是compile的實(shí)現(xiàn)是Bail版的
??hook.compile?=?function(options)?{
????????const?{?taps?}?=?options;
????????const?call?=?function(...args)?{
????????????const?tapsLength?=?taps.length;
????????????for(let?i?=?0;?i?????????????????const?fn?=?this.taps[i];
????????????????const?res?=?fn(...args);
????????????????if(?res?!==?undefined)?break;
????????????}
????????}
????????return?call;
????};
????
?return?hook;
}
SyncBailHook.prototype?=?null;
抽象代碼工廠
上面我們通過對(duì)SyncHook和SyncBailHook的抽象提煉出了一個(gè)基類Hook,減少了重復(fù)代碼。基于這種結(jié)構(gòu)子類需要實(shí)現(xiàn)的就是complie方法,但是如果我們將SyncHook和SyncBailHook的complie方法拿出來(lái)對(duì)比下:
SyncHook:
hook.compile?=?function(options)?{
??const?{?taps?}?=?options;
??const?call?=?function(...args)?{
????const?tapsLength?=?taps.length;
????for(let?i?=?0;?i???????const?fn?=?this.taps[i];
??????fn(...args);
????}
??}
??return?call;
};
SyncBailHook:
hook.compile?=?function(options)?{
??const?{?taps?}?=?options;
??const?call?=?function(...args)?{
????const?tapsLength?=?taps.length;
????for(let?i?=?0;?i???????const?fn?=?this.taps[i];
??????const?res?=?fn(...args);
??????if(?res?!==?undefined)?return?res;
????}
??}
??return?call;
};
我們發(fā)現(xiàn)這兩個(gè)complie也非常像,有大量重復(fù)代碼,所以tapable為了解決這些重復(fù)代碼,又進(jìn)行了一次抽象,也就是代碼工廠HookCodeFactory。HookCodeFactory的作用就是用來(lái)生成complie返回的call函數(shù)體,而HookCodeFactory在實(shí)現(xiàn)時(shí)也采用了Hook類似的思路,也是先實(shí)現(xiàn)了一個(gè)基類HookCodeFactory,然后不同的Hook再繼承這個(gè)類來(lái)實(shí)現(xiàn)自己的代碼工廠,比如SyncHookCodeFactory。
創(chuàng)建函數(shù)的方法
在繼續(xù)深入代碼工廠前,我們先來(lái)回顧下JS里面創(chuàng)建函數(shù)的方法。一般我們會(huì)有這幾種方法:
函數(shù)申明
function?add(a,?b)?{
??return?a?+?b;
}函數(shù)表達(dá)式
const?add?=?function(a,?b)?{
??return?a?+?b;
}
但是除了這兩種方法外,還有種不常用的方法:使用Function構(gòu)造函數(shù)。比如上面這個(gè)函數(shù)使用構(gòu)造函數(shù)創(chuàng)建就是這樣的:
const?add?=?new?Function('a',?'b',?'return?a?+?b;');
上面的調(diào)用形式里,最后一個(gè)參數(shù)是函數(shù)的函數(shù)體,前面的參數(shù)都是函數(shù)的形參,最終生成的函數(shù)跟用函數(shù)表達(dá)式的效果是一樣的,可以這樣調(diào)用:
add(1,?2);????//?結(jié)果是3
注意:上面的a和b形參放在一起用逗號(hào)隔開也是可以的:
const?add?=?new?Function('a,?b',?'return?a?+?b;');????//?這樣跟上面的效果是一樣的
當(dāng)然函數(shù)并不是一定要有參數(shù),沒有參數(shù)的函數(shù)也可以這樣創(chuàng)建:
const?sayHi?=?new?Function('alert("Hello")');
sayHi();?//?Hello
這樣創(chuàng)建函數(shù)和前面的函數(shù)申明和函數(shù)表達(dá)式有什么區(qū)別呢?使用Function構(gòu)造函數(shù)來(lái)創(chuàng)建函數(shù)最大的一個(gè)特征就是,函數(shù)體是一個(gè)字符串,也就是說(shuō)我們可以動(dòng)態(tài)生成這個(gè)字符串,從而動(dòng)態(tài)生成函數(shù)體。因?yàn)?code style="font-size:14px;color:rgb(30,107,184);background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">SyncHook和SyncBailHook的call函數(shù)很像,我們可以像拼一個(gè)字符串那樣拼出他們的函數(shù)體,為了更簡(jiǎn)單的拼湊,tapable最終生成的call函數(shù)里面并沒有循環(huán),而是在拼函數(shù)體的時(shí)候就將循環(huán)展開了,比如SyncHook拼出來(lái)的call函數(shù)的函數(shù)體就是這樣的:
"use?strict";
var?_x?=?this._x;
var?_fn0?=?_x[0];
_fn0(newSpeed);
var?_fn1?=?_x[1];
_fn1(newSpeed);
上面代碼的_x其實(shí)就是保存回調(diào)的數(shù)組taps,這里重命名為_x,我想是為了節(jié)省代碼大小吧。這段代碼可以看到,_x,也就是taps里面的內(nèi)容已經(jīng)被展開了,是一個(gè)一個(gè)取出來(lái)執(zhí)行的。
而SyncBailHook最終生成的call函數(shù)體是這樣的:
"use?strict";
var?_x?=?this._x;
var?_fn0?=?_x[0];
var?_result0?=?_fn0(newSpeed);
if?(_result0?!==?undefined)?{
????return?_result0;
????;
}?else?{
????var?_fn1?=?_x[1];
????var?_result1?=?_fn1(newSpeed);
????if?(_result1?!==?undefined)?{
????????return?_result1;
????????;
????}?else?{
????}
}
這段生成的代碼主體邏輯其實(shí)跟SyncHook是一樣的,都是將_x展開執(zhí)行了,他們的區(qū)別是SyncBailHook會(huì)對(duì)每次執(zhí)行的結(jié)果進(jìn)行檢測(cè),如果結(jié)果不是undefined就直接return了,后面的回調(diào)函數(shù)就沒有機(jī)會(huì)執(zhí)行了。
創(chuàng)建代碼工廠基類
基于這個(gè)目的,我們的代碼工廠基類應(yīng)該可以生成最基本的call函數(shù)體。我們來(lái)寫個(gè)最基本的HookCodeFactory吧,目前他只能生成SyncHook的call函數(shù)體:
class?HookCodeFactory?{
????constructor()?{
????????//?構(gòu)造函數(shù)定義兩個(gè)變量
????????this.options?=?undefined;
????????this._args?=?undefined;
????}
????//?init函數(shù)初始化變量
????init(options)?{
????????this.options?=?options;
????????this._args?=?options.args.slice();
????}
????//?deinit重置變量
????deinit()?{
????????this.options?=?undefined;
????????this._args?=?undefined;
????}
????//?args用來(lái)將傳入的數(shù)組args轉(zhuǎn)換為New?Function接收的逗號(hào)分隔的形式
????//?['arg1',?'args']?--->??'arg1,?arg2'
????args()?{
????????return?this._args.join(",?");
????}
????//?setup其實(shí)就是給生成代碼的_x賦值
????setup(instance,?options)?{
????????instance._x?=?options.taps.map(t?=>?t);
????}
????//?create創(chuàng)建最終的call函數(shù)
????create(options)?{
????????this.init(options);
????????let?fn;
????????//?直接將taps展開為平鋪的函數(shù)調(diào)用
????????const?{?taps?}?=?options;
????????let?code?=?'';
????????for?(let?i?=?0;?i?????????????code?+=?`
????????????????var?_fn${i}?=?_x[${i}];
????????????????_fn${i}(${this.args()});
????????????`
????????}
????????//?將展開的循環(huán)和頭部連接起來(lái)
????????const?allCodes?=?`
????????????"use?strict";
????????????var?_x?=?this._x;
????????`?+?code;
????????//?用傳進(jìn)來(lái)的參數(shù)和生成的函數(shù)體創(chuàng)建一個(gè)函數(shù)出來(lái)
????????fn?=?new?Function(this.args(),?allCodes);
????????this.deinit();??//?重置變量
????????return?fn;????//?返回生成的函數(shù)
????}
}
上面代碼最核心的其實(shí)就是create函數(shù),這個(gè)函數(shù)會(huì)動(dòng)態(tài)創(chuàng)建一個(gè)call函數(shù)并返回,所以SyncHook可以直接使用這個(gè)factory創(chuàng)建代碼了:
//?SyncHook.js
const?Hook?=?require('./Hook');
const?HookCodeFactory?=?require("./HookCodeFactory");
const?factory?=?new?HookCodeFactory();
//?COMPILE函數(shù)會(huì)去調(diào)用factory來(lái)生成call函數(shù)
const?COMPILE?=?function(options)?{
?factory.setup(this,?options);
?return?factory.create(options);
};
function?SyncHook(args?=?[])?{
??const?hook?=?new?Hook(args);
????hook.constructor?=?SyncHook;
????//?使用HookCodeFactory來(lái)創(chuàng)建最終的call函數(shù)
????hook.compile?=?COMPILE;
?return?hook;
}
SyncHook.prototype?=?null;
讓代碼工廠支持SyncBailHook
現(xiàn)在我們的HookCodeFactory只能生成最簡(jiǎn)單的SyncHook代碼,我們需要對(duì)他進(jìn)行一些改進(jìn),讓他能夠也生成SyncBailHook的call函數(shù)體。你可以拉回前面再仔細(xì)觀察下這兩個(gè)最終生成代碼的區(qū)別:
SyncBailHook需要對(duì)每次執(zhí)行的result進(jìn)行處理,如果不為undefined就返回SyncBailHook生成的代碼其實(shí)是if...else嵌套的,我們生成的時(shí)候可以考慮使用一個(gè)遞歸函數(shù)
為了讓SyncHook和SyncBailHook的子類代碼工廠能夠傳入差異化的result處理,我們先將HookCodeFactory基類的create拆成兩部分,將代碼拼裝的邏輯單獨(dú)拆成一個(gè)函數(shù):
class?HookCodeFactory?{
????//?...
???//?省略其他一樣的代碼
???//?...
????//?create創(chuàng)建最終的call函數(shù)
????create(options)?{
????????this.init(options);
????????let?fn;
????????//?拼裝代碼頭部
????????const?header?=?`
????????????"use?strict";
????????????var?_x?=?this._x;
????????`;
????????//?用傳進(jìn)來(lái)的參數(shù)和函數(shù)體創(chuàng)建一個(gè)函數(shù)出來(lái)
????????fn?=?new?Function(this.args(),
????????????header?+
????????????this.content());?????????//?注意這里的content函數(shù)并沒有在基類HookCodeFactory實(shí)現(xiàn),而是子類實(shí)現(xiàn)的
????????this.deinit();
????????return?fn;
????}
????//?拼裝函數(shù)體
???//?callTapsSeries也沒在基類調(diào)用,而是子類調(diào)用的
????callTapsSeries()?{
????????const?{?taps?}?=?this.options;
????????let?code?=?'';
????????for?(let?i?=?0;?i?????????????code?+=?`
????????????????var?_fn${i}?=?_x[${i}];
????????????????_fn${i}(${this.args()});
????????????`
????????}
????????return?code;
????}
}
上面代碼里面要特別注意create函數(shù)里面生成函數(shù)體的時(shí)候調(diào)用的是this.content,但是this.content并沒與在基類實(shí)現(xiàn),這要求子類在使用HookCodeFactory的時(shí)候都需要繼承他并實(shí)現(xiàn)自己的content函數(shù),所以這里的content函數(shù)也是一個(gè)抽象接口。那SyncHook的代碼就應(yīng)該改成這樣:
//?SyncHook.js
//?...?省略其他一樣的代碼?...
//?SyncHookCodeFactory繼承HookCodeFactory并實(shí)現(xiàn)content函數(shù)
class?SyncHookCodeFactory?extends?HookCodeFactory?{
????content()?{
????????return?this.callTapsSeries();????//?這里的callTapsSeries是基類的
????}
}
//?使用SyncHookCodeFactory來(lái)創(chuàng)建factory
const?factory?=?new?SyncHookCodeFactory();
const?COMPILE?=?function?(options)?{
????factory.setup(this,?options);
????return?factory.create(options);
};
**注意這里:**子類實(shí)現(xiàn)的content其實(shí)又調(diào)用了基類的callTapsSeries來(lái)生成最終的函數(shù)體。所以這里這幾個(gè)函數(shù)的調(diào)用關(guān)系其實(shí)是這樣的:
image-20210401111739814那這樣設(shè)計(jì)的目的是什么呢?為了讓子類content能夠傳遞參數(shù)給基類callTapsSeries,從而生成不一樣的函數(shù)體。我們馬上就能在SyncBailHook的代碼工廠上看到了。
為了能夠生成SyncBailHook的函數(shù)體,我們需要讓callTapsSeries支持一個(gè)onResult參數(shù),就是這樣:
class?HookCodeFactory?{
????//?...?省略其他相同的代碼?...
????//?拼裝函數(shù)體,需要支持options.onResult參數(shù)
????callTapsSeries(options)?{
????????const?{?taps?}?=?this.options;
????????let?code?=?'';
????????let?i?=?0;
????????const?onResult?=?options?&&?options.onResult;
????????
????????//?寫一個(gè)next函數(shù)來(lái)開啟有onResult回調(diào)的函數(shù)體生成
????????//?next和onResult相互遞歸調(diào)用來(lái)生成最終的函數(shù)體
????????const?next?=?()?=>?{
????????????if(i?>=?taps.length)?return?'';
????????????const?result?=?`_result${i}`;
????????????const?code?=?`
????????????????var?_fn${i}?=?_x[${i}];
????????????????var?${result}?=?_fn${i}(${this.args()});
????????????????${onResult(i++,?result,?next)}
????????????`;
????????????return?code;
????????}
????????//?支持onResult參數(shù)
????????if(onResult)?{
????????????code?=?next();
????????}?else?{
???????????//?沒有onResult參數(shù)的時(shí)候,即SyncHook跟之前保持一樣
????????????for(;?i????????????????code?+=?`
????????????????????var?_fn${i}?=?_x[${i}];
????????????????????_fn${i}(${this.args()});
????????????????`
????????????}
????????}
????????return?code;
????}
}
然后我們的SyncBailHook的代碼工廠在繼承工廠基類的時(shí)候需要傳一個(gè)onResult參數(shù),就是這樣:
const?Hook?=?require('./Hook');
const?HookCodeFactory?=?require("./HookCodeFactory");
//?SyncBailHookCodeFactory繼承HookCodeFactory并實(shí)現(xiàn)content函數(shù)
//?content里面?zhèn)魅攵ㄖ频膐nResult函數(shù),onResult回去調(diào)用next遞歸生成嵌套的if...else...
class?SyncBailHookCodeFactory?extends?HookCodeFactory?{
????content()?{
????????return?this.callTapsSeries({
????????????onResult:?(i,?result,?next)?=>
????????????????`if(${result}?!==?undefined)?{\nreturn?${result};\n}?else?{\n${next()}}\n`,
????????});
????}
}
//?使用SyncHookCodeFactory來(lái)創(chuàng)建factory
const?factory?=?new?SyncBailHookCodeFactory();
const?COMPILE?=?function?(options)?{
????factory.setup(this,?options);
????return?factory.create(options);
};
function?SyncBailHook(args?=?[])?{
????//?基本結(jié)構(gòu)跟SyncHook都是一樣的
????const?hook?=?new?Hook(args);
????hook.constructor?=?SyncBailHook;
????//?使用HookCodeFactory來(lái)創(chuàng)建最終的call函數(shù)
????hook.compile?=?COMPILE;
????return?hook;
}
現(xiàn)在運(yùn)行下代碼,效果跟之前一樣的,大功告成~
其他Hook的實(shí)現(xiàn)
到這里,tapable的源碼架構(gòu)和基本實(shí)現(xiàn)我們已經(jīng)弄清楚了,但是本文只用了SyncHook和SyncBailHook做例子,其他的,比如AsyncParallelHook并沒有展開講。因?yàn)?code style="font-size:14px;color:rgb(30,107,184);background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">AsyncParallelHook之類的其他Hook的實(shí)現(xiàn)思路跟本文是一樣的,比如我們可以先實(shí)現(xiàn)一個(gè)獨(dú)立的AsyncParallelHook類:
class?AsyncParallelHook?{
????constructor(args?=?[])?{
????????this._args?=?args;
????????this.taps?=?[];
????}
????tapAsync(name,?task)?{
????????this.taps.push(task);
????}
????callAsync(...args)?{
????????//?先取出最后傳入的回調(diào)函數(shù)
????????let?finalCallback?=?args.pop();
????????//?定義一個(gè)?i?變量和?done?函數(shù),每次執(zhí)行檢測(cè)?i?值和隊(duì)列長(zhǎng)度,決定是否執(zhí)行?callAsync?的最終回調(diào)函數(shù)
????????let?i?=?0;
????????let?done?=?()?=>?{
????????????if?(++i?===?this.taps.length)?{
????????????????finalCallback();
????????????}
????????};
????????//?依次執(zhí)行事件處理函數(shù)
????????this.taps.forEach(task?=>?task(...args,?done));
????}
}
然后對(duì)他的callAsync函數(shù)進(jìn)行抽象,將其抽象到代碼工廠類里面,使用字符串拼接的方式動(dòng)態(tài)構(gòu)造出來(lái)就行了,整體思路跟前面是一樣的。具體實(shí)現(xiàn)過程可以參考tapable源碼:
Hook類源碼
SyncHook類源碼
SyncBailHook類源碼
HookCodeFactory類源碼
總結(jié)
本文可運(yùn)行示例代碼已經(jīng)上傳GitHub,大家拿下來(lái)一邊玩一邊看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-source-code。
下面再對(duì)本文的思路進(jìn)行一個(gè)總結(jié):
tapable的各種Hook其實(shí)都是基于發(fā)布訂閱模式。- 各個(gè)
Hook自己獨(dú)立實(shí)現(xiàn)其實(shí)也沒有問題,但是因?yàn)槎际前l(fā)布訂閱模式,會(huì)有大量重復(fù)代碼,所以tapable進(jìn)行了幾次抽象。 - 第一次抽象是提取一個(gè)
Hook基類,這個(gè)基類實(shí)現(xiàn)了初始化和事件注冊(cè)等公共部分,至于每個(gè)Hook的call都不一樣,需要自己實(shí)現(xiàn)。 - 第二次抽象是每個(gè)
Hook在實(shí)現(xiàn)自己的call的時(shí)候,發(fā)現(xiàn)代碼也有很多相似之處,所以提取了一個(gè)代碼工廠,用來(lái)動(dòng)態(tài)生成call的函數(shù)體。 - 總體來(lái)說(shuō),
tapable的代碼并不難,但是因?yàn)橛袃纱纬橄螅麄€(gè)代碼架構(gòu)顯得不那么好讀,經(jīng)過本文的梳理后,應(yīng)該會(huì)好很多了。
覺得博主寫得還可以的話,不要忘了分享、點(diǎn)贊、在看三連哦~
長(zhǎng)按下方圖片,關(guān)注進(jìn)擊的大前端,獲取更多的優(yōu)質(zhì)原創(chuàng)文章~?
參考資料
tapable用法介紹:https://juejin.cn/post/6939794845053485093
tapable源碼地址:https://github.com/webpack/tapable
