上一篇文章我寫了tapable
的基本用法,咱們知道他是一個加強版版的發佈訂閱模式
,本文想來學習下他的源碼。tapable
的源碼我讀了一下,發現他的抽象程度比較高,直接扎進去反而會讓人云裏霧裏的,因此本文會從最簡單的SyncHook
和發佈訂閱模式
入手,再一步一步抽象,慢慢變成他源碼的樣子。javascript
本文可運行示例代碼已經上傳GitHub,你們拿下來一邊玩一邊看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-source-code。前端
SyncHook
的基本實現上一篇文章已經講過SyncHook
的用法了,我這裏就再也不展開了,他使用的例子就是這樣子:java
const { SyncHook } = require("tapable"); // 實例化一個加速的hook const accelerate = new SyncHook(["newSpeed"]); // 註冊第一個回調,加速時記錄下當前速度 accelerate.tap("LoggerPlugin", (newSpeed) => console.log("LoggerPlugin", `加速到${newSpeed}`) ); // 再註冊一個回調,用來檢測是否超速 accelerate.tap("OverspeedPlugin", (newSpeed) => { if (newSpeed > 120) { console.log("OverspeedPlugin", "您已超速!!"); } }); // 觸發一下加速事件,看看效果吧 accelerate.call(500);
其實這種用法就是一個最基本的發佈訂閱模式
,我以前講發佈訂閱模式的文章講過,咱們能夠仿照那個很快實現一個SyncHook
:webpack
class SyncHook { constructor(args = []) { this._args = args; // 接收的參數存下來 this.taps = []; // 一個存回調的數組 } // tap實例方法用來註冊回調 tap(name, fn) { // 邏輯很簡單,直接保存下傳入的回調參數就行 this.taps.push(fn); } // call實例方法用來觸發事件,執行全部回調 call(...args) { // 邏輯也很簡單,將註冊的回調一個一個拿出來執行就行 const tapsLength = this.taps.length; for(let i = 0; i < tapsLength; i++) { const fn = this.taps[i]; fn(...args); } } }
這段代碼很是簡單,是一個最基礎的發佈訂閱模式
,使用方法跟上面是同樣的,將SyncHook
從tapable
導出改成使用咱們本身的:git
// const { SyncHook } = require("tapable"); const { SyncHook } = require("./SyncHook");
運行效果是同樣的:github
注意: 咱們構造函數裏面傳入的args
並無用上,tapable
主要是用它來動態生成call
的函數體的,在後面講代碼工廠的時候會看到。web
SyncBailHook
的基本實現再來一個SyncBailHook
的基本實現吧,SyncBailHook
的做用是當前一個回調返回不爲undefined
的值的時候,阻止後面的回調執行。基本使用是這樣的:segmentfault
const { SyncBailHook } = require("tapable"); // 使用的是SyncBailHook const accelerate = new SyncBailHook(["newSpeed"]); accelerate.tap("LoggerPlugin", (newSpeed) => console.log("LoggerPlugin", `加速到${newSpeed}`) ); // 再註冊一個回調,用來檢測是否超速 // 若是超速就返回一個錯誤 accelerate.tap("OverspeedPlugin", (newSpeed) => { if (newSpeed > 120) { console.log("OverspeedPlugin", "您已超速!!"); return new Error('您已超速!!'); } }); // 因爲上一個回調返回了一個不爲undefined的值 // 這個回調不會再運行了 accelerate.tap("DamagePlugin", (newSpeed) => { if (newSpeed > 300) { console.log("DamagePlugin", "速度實在太快,車子快散架了。。。"); } }); accelerate.call(500);
他的實現跟上面的SyncHook
也很是像,只是call
在執行的時候不同而已,SyncBailHook
須要檢測每一個回調的返回值,若是不爲undefined
就終止執行後面的回調,因此代碼實現以下:數組
class SyncBailHook { constructor(args = []) { this._args = args; this.taps = []; } tap(name, fn) { this.taps.push(fn); } // 其餘代碼跟SyncHook是同樣的,就是call的實現不同 // 須要檢測每一個返回值,若是不爲undefined就終止執行 call(...args) { const tapsLength = this.taps.length; for(let i = 0; i < tapsLength; i++) { const fn = this.taps[i]; const res = fn(...args); if( res !== undefined) return res; } } }
而後改下SyncBailHook
從咱們本身的引入就行:架構
// const { SyncBailHook } = require("tapable"); const { SyncBailHook } = require("./SyncBailHook");
運行效果是同樣的:
如今咱們只實現了SyncHook
和SyncBailHook
兩個Hook
而已,上一篇講用法的文章裏面總共有9個Hook
,若是每一個Hook
都像前面這樣實現也是能夠的。可是咱們再仔細看下SyncHook
和SyncBailHook
兩個類的代碼,發現他們除了call
的實現不同,其餘代碼如出一轍,因此做爲一個有追求的工程師,咱們能夠把這部分重複的代碼提出來做爲一個基類:Hook
類。
Hook
類須要包含一些公共的代碼,call
這種不同的部分由各個子類本身實現。因此Hook
類就長這樣:
const CALL_DELEGATE = function(...args) { this.call = this._createCall(); return this.call(...args); }; // Hook是SyncHook和SyncBailHook的基類 // 大致結構是同樣的,不同的地方是call // 不一樣子類的call是不同的 // tapable的Hook基類提供了一個抽象接口compile來動態生成call函數 class Hook { constructor(args = []) { this._args = args; this.taps = []; // 基類的call初始化爲CALL_DELEGATE // 爲何這裏須要這樣一個代理,而不是直接this.call = _createCall() // 等咱們後面子類實現了再一塊兒講 this.call = CALL_DELEGATE; } // 一個抽象接口compile // 由子類實現,基類compile不能直接調用 compile(options) { throw new Error("Abstract: should be overridden"); } tap(name, fn) { this.taps.push(fn); } // _createCall調用子類實現的compile來生成call方法 _createCall() { return this.compile({ taps: this.taps, args: this._args, }); } }
官方對應的源碼看這裏:https://github.com/webpack/tapable/blob/master/lib/Hook.js
如今有了Hook
基類,咱們的SyncHook
就須要繼承這個基類重寫,tapable
在這裏繼承的時候並無使用class extends
,而是手動繼承的:
const Hook = require('./Hook'); function SyncHook(args = []) { // 先手動繼承Hook const hook = new Hook(args); hook.constructor = SyncHook; // 而後實現本身的compile函數 // compile的做用應該是建立一個call函數並返回 hook.compile = function(options) { // 這裏call函數的實現跟前面實現是同樣的 const { taps } = options; const call = function(...args) { const tapsLength = taps.length; for(let i = 0; i < tapsLength; i++) { const fn = this.taps[i]; fn(...args); } } return call; }; return hook; } SyncHook.prototype = null;
注意:咱們在基類Hook
構造函數中初始化this.call
爲CALL_DELEGATE
這個函數,這是有緣由的,最主要的緣由是確保this
的正確指向。思考一下假如咱們不用CALL_DELEGATE
,而是直接this.call = this._createCall()
會發生什麼?咱們來分析下這個執行流程:
new SyncHook()
,這時候會執行const hook = new Hook(args);
new Hook(args)
會去執行Hook
的構造函數,也就是會運行this.call = this._createCall()
this
指向的是基類Hook
的實例,this._createCall()
會調用基類的this.compile()
complie
函數是一個抽象接口,直接調用會報錯Abstract: should be overridden
。那咱們採用this.call = CALL_DELEGATE
是怎麼解決這個問題的呢?
this.call = CALL_DELEGATE
後,基類Hook
上的call
就只是被賦值爲一個代理函數而已,這個函數不會立馬調用。new SyncHook()
,裏面會執行Hook
的構造函數Hook
構造函數會給this.call
賦值爲CALL_DELEGATE
,可是不會當即執行。new SyncHook()
繼續執行,新建的實例上的方法hook.complie
被覆寫爲正確方法。hook.call
的時候纔會真正執行this._createCall()
,這裏面會去調用this.complie()
complie
已是被正確覆寫過的了,因此獲得正確的結果。子類SyncBailHook
的實現跟上面SyncHook
的也是很是像,只是hook.compile
實現不同而已:
const Hook = require('./Hook'); function SyncBailHook(args = []) { // 基本結構跟SyncHook都是同樣的 const hook = new Hook(args); hook.constructor = SyncBailHook; // 只是compile的實現是Bail版的 hook.compile = function(options) { const { taps } = options; const call = function(...args) { const tapsLength = taps.length; for(let i = 0; i < tapsLength; i++) { const fn = this.taps[i]; const res = fn(...args); if( res !== undefined) break; } } return call; }; return hook; } SyncBailHook.prototype = null;
上面咱們經過對SyncHook
和SyncBailHook
的抽象提煉出了一個基類Hook
,減小了重複代碼。基於這種結構子類須要實現的就是complie
方法,可是若是咱們將SyncHook
和SyncBailHook
的complie
方法拿出來對比下:
SyncHook:
hook.compile = function(options) { const { taps } = options; const call = function(...args) { const tapsLength = taps.length; for(let i = 0; i < tapsLength; 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 < tapsLength; i++) { const fn = this.taps[i]; const res = fn(...args); if( res !== undefined) return res; } } return call; };
咱們發現這兩個complie
也很是像,有大量重複代碼,因此tapable
爲了解決這些重複代碼,又進行了一次抽象,也就是代碼工廠HookCodeFactory
。HookCodeFactory
的做用就是用來生成complie
返回的call
函數體,而HookCodeFactory
在實現時也採用了Hook
相似的思路,也是先實現了一個基類HookCodeFactory
,而後不一樣的Hook
再繼承這個類來實現本身的代碼工廠,好比SyncHookCodeFactory
。
在繼續深刻代碼工廠前,咱們先來回顧下JS裏面建立函數的方法。通常咱們會有這幾種方法:
函數申明
function add(a, b) { return a + b; }
函數表達式
const add = function(a, b) { return a + b; }
可是除了這兩種方法外,還有種不經常使用的方法:使用Function構造函數。好比上面這個函數使用構造函數建立就是這樣的:
const add = new Function('a', 'b', 'return a + b;');
上面的調用形式裏,最後一個參數是函數的函數體,前面的參數都是函數的形參,最終生成的函數跟用函數表達式的效果是同樣的,能夠這樣調用:
add(1, 2); // 結果是3
注意:上面的a
和b
形參放在一塊兒用逗號隔開也是能夠的:
const add = new Function('a, b', 'return a + b;'); // 這樣跟上面的效果是同樣的
固然函數並非必定要有參數,沒有參數的函數也能夠這樣建立:
const sayHi = new Function('alert("Hello")'); sayHi(); // Hello
這樣建立函數和前面的函數申明和函數表達式有什麼區別呢?使用Function構造函數來建立函數最大的一個特徵就是,函數體是一個字符串,也就是說咱們能夠動態生成這個字符串,從而動態生成函數體。由於SyncHook
和SyncBailHook
的call
函數很像,咱們能夠像拼一個字符串那樣拼出他們的函數體,爲了更簡單的拼湊,tapable
最終生成的call
函數裏面並無循環,而是在拼函數體的時候就將循環展開了,好比SyncHook
拼出來的call
函數的函數體就是這樣的:
"use strict"; var _x = this._x; var _fn0 = _x[0]; _fn0(newSpeed); var _fn1 = _x[1]; _fn1(newSpeed);
上面代碼的_x
其實就是保存回調的數組taps
,這裏重命名爲_x
,我想是爲了節省代碼大小吧。這段代碼能夠看到,_x
,也就是taps
裏面的內容已經被展開了,是一個一個取出來執行的。
而SyncBailHook
最終生成的call
函數體是這樣的:
"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 { } }
這段生成的代碼主體邏輯其實跟SyncHook
是同樣的,都是將_x
展開執行了,他們的區別是SyncBailHook
會對每次執行的結果進行檢測,若是結果不是undefined
就直接return
了,後面的回調函數就沒有機會執行了。
基於這個目的,咱們的代碼工廠基類應該能夠生成最基本的call
函數體。咱們來寫個最基本的HookCodeFactory
吧,目前他只能生成SyncHook
的call
函數體:
class HookCodeFactory { constructor() { // 構造函數定義兩個變量 this.options = undefined; this._args = undefined; } // init函數初始化變量 init(options) { this.options = options; this._args = options.args.slice(); } // deinit重置變量 deinit() { this.options = undefined; this._args = undefined; } // args用來將傳入的數組args轉換爲New Function接收的逗號分隔的形式 // ['arg1', 'args'] ---> 'arg1, arg2' args() { return this._args.join(", "); } // setup其實就是給生成代碼的_x賦值 setup(instance, options) { instance._x = options.taps.map(t => t); } // create建立最終的call函數 create(options) { this.init(options); let fn; // 直接將taps展開爲平鋪的函數調用 const { taps } = options; let code = ''; for (let i = 0; i < taps.length; i++) { code += ` var _fn${i} = _x[${i}]; _fn${i}(${this.args()}); ` } // 將展開的循環和頭部鏈接起來 const allCodes = ` "use strict"; var _x = this._x; ` + code; // 用傳進來的參數和生成的函數體建立一個函數出來 fn = new Function(this.args(), allCodes); this.deinit(); // 重置變量 return fn; // 返回生成的函數 } }
上面代碼最核心的其實就是create
函數,這個函數會動態建立一個call
函數並返回,因此SyncHook
能夠直接使用這個factory
建立代碼了:
// SyncHook.js const Hook = require('./Hook'); const HookCodeFactory = require("./HookCodeFactory"); const factory = new HookCodeFactory(); // COMPILE函數會去調用factory來生成call函數 const COMPILE = function(options) { factory.setup(this, options); return factory.create(options); }; function SyncHook(args = []) { const hook = new Hook(args); hook.constructor = SyncHook; // 使用HookCodeFactory來建立最終的call函數 hook.compile = COMPILE; return hook; } SyncHook.prototype = null;
SyncBailHook
如今咱們的HookCodeFactory
只能生成最簡單的SyncHook
代碼,咱們須要對他進行一些改進,讓他可以也生成SyncBailHook
的call
函數體。你能夠拉回前面再仔細觀察下這兩個最終生成代碼的區別:
SyncBailHook
須要對每次執行的result
進行處理,若是不爲undefined
就返回SyncBailHook
生成的代碼實際上是if...else
嵌套的,咱們生成的時候能夠考慮使用一個遞歸函數爲了讓SyncHook
和SyncBailHook
的子類代碼工廠可以傳入差別化的result
處理,咱們先將HookCodeFactory
基類的create
拆成兩部分,將代碼拼裝的邏輯單獨拆成一個函數:
class HookCodeFactory { // ... // 省略其餘同樣的代碼 // ... // create建立最終的call函數 create(options) { this.init(options); let fn; // 拼裝代碼頭部 const header = ` "use strict"; var _x = this._x; `; // 用傳進來的參數和函數體建立一個函數出來 fn = new Function(this.args(), header + this.content()); // 注意這裏的content函數並無在基類HookCodeFactory實現,而是子類實現的 this.deinit(); return fn; } // 拼裝函數體 // callTapsSeries也沒在基類調用,而是子類調用的 callTapsSeries() { const { taps } = this.options; let code = ''; for (let i = 0; i < taps.length; i++) { code += ` var _fn${i} = _x[${i}]; _fn${i}(${this.args()}); ` } return code; } }
上面代碼裏面要特別注意create
函數裏面生成函數體的時候調用的是this.content
,可是this.content
並沒與在基類實現,這要求子類在使用HookCodeFactory
的時候都須要繼承他並實現本身的content
函數,因此這裏的content
函數也是一個抽象接口。那SyncHook
的代碼就應該改爲這樣:
// SyncHook.js // ... 省略其餘同樣的代碼 ... // SyncHookCodeFactory繼承HookCodeFactory並實現content函數 class SyncHookCodeFactory extends HookCodeFactory { content() { return this.callTapsSeries(); // 這裏的callTapsSeries是基類的 } } // 使用SyncHookCodeFactory來建立factory const factory = new SyncHookCodeFactory(); const COMPILE = function (options) { factory.setup(this, options); return factory.create(options); };
注意這裏:子類實現的content
其實又調用了基類的callTapsSeries
來生成最終的函數體。因此這裏這幾個函數的調用關係實際上是這樣的:
那這樣設計的目的是什麼呢?爲了讓子類content
可以傳遞參數給基類callTapsSeries
,從而生成不同的函數體。咱們立刻就能在SyncBailHook
的代碼工廠上看到了。
爲了可以生成SyncBailHook
的函數體,咱們須要讓callTapsSeries
支持一個onResult
參數,就是這樣:
class HookCodeFactory { // ... 省略其餘相同的代碼 ... // 拼裝函數體,須要支持options.onResult參數 callTapsSeries(options) { const { taps } = this.options; let code = ''; let i = 0; const onResult = options && options.onResult; // 寫一個next函數來開啓有onResult回調的函數體生成 // next和onResult相互遞歸調用來生成最終的函數體 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參數 if(onResult) { code = next(); } else { // 沒有onResult參數的時候,即SyncHook跟以前保持同樣 for(; i< taps.length; i++) { code += ` var _fn${i} = _x[${i}]; _fn${i}(${this.args()}); ` } } return code; } }
而後咱們的SyncBailHook
的代碼工廠在繼承工廠基類的時候須要傳一個onResult
參數,就是這樣:
const Hook = require('./Hook'); const HookCodeFactory = require("./HookCodeFactory"); // SyncBailHookCodeFactory繼承HookCodeFactory並實現content函數 // content裏面傳入定製的onResult函數,onResult回去調用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來建立factory const factory = new SyncBailHookCodeFactory(); const COMPILE = function (options) { factory.setup(this, options); return factory.create(options); }; function SyncBailHook(args = []) { // 基本結構跟SyncHook都是同樣的 const hook = new Hook(args); hook.constructor = SyncBailHook; // 使用HookCodeFactory來建立最終的call函數 hook.compile = COMPILE; return hook; }
如今運行下代碼,效果跟以前同樣的,大功告成~
到這裏,tapable
的源碼架構和基本實現咱們已經弄清楚了,可是本文只用了SyncHook
和SyncBailHook
作例子,其餘的,好比AsyncParallelHook
並無展開講。由於AsyncParallelHook
之類的其餘Hook
的實現思路跟本文是同樣的,好比咱們能夠先實現一個獨立的AsyncParallelHook
類:
class AsyncParallelHook { constructor(args = []) { this._args = args; this.taps = []; } tapAsync(name, task) { this.taps.push(task); } callAsync(...args) { // 先取出最後傳入的回調函數 let finalCallback = args.pop(); // 定義一個 i 變量和 done 函數,每次執行檢測 i 值和隊列長度,決定是否執行 callAsync 的最終回調函數 let i = 0; let done = () => { if (++i === this.taps.length) { finalCallback(); } }; // 依次執行事件處理函數 this.taps.forEach(task => task(...args, done)); } }
而後對他的callAsync
函數進行抽象,將其抽象到代碼工廠類裏面,使用字符串拼接的方式動態構造出來就好了,總體思路跟前面是同樣的。具體實現過程能夠參考tapable
源碼:
本文可運行示例代碼已經上傳GitHub,你們拿下來一邊玩一邊看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-source-code。
下面再對本文的思路進行一個總結:
tapable
的各類Hook
其實都是基於發佈訂閱模式。Hook
本身獨立實現其實也沒有問題,可是由於都是發佈訂閱模式,會有大量重複代碼,因此tapable
進行了幾回抽象。Hook
基類,這個基類實現了初始化和事件註冊等公共部分,至於每一個Hook
的call
都不同,須要本身實現。Hook
在實現本身的call
的時候,發現代碼也有不少類似之處,因此提取了一個代碼工廠,用來動態生成call
的函數體。tapable
的代碼並不難,可是由於有兩次抽象,整個代碼架構顯得不那麼好讀,通過本文的梳理後,應該會好不少了。文章的最後,感謝你花費寶貴的時間閱讀本文,若是本文給了你一點點幫助或者啓發,請不要吝嗇你的贊和GitHub小星星,你的支持是做者持續創做的動力。
歡迎關注個人公衆號進擊的大前端第一時間獲取高質量原創~
「前端進階知識」系列文章源碼地址: https://github.com/dennis-jiang/Front-End-Knowledges
tapable
用法介紹:http://www.javashuo.com/article/p-rgjasasi-vg.html
tapable
源碼地址:https://github.com/webpack/tapable