前不久寫了一篇webpack基本原理和AST用法的文章,原本想接着寫webpack plugin
的原理的,可是發現webpack plugin
高度依賴tapable這個庫,不清楚tapable
而直接去看webpack plugin
始終有點霧裏看花的意思。因此就先去看了下tapable
的文檔和源碼,發現這個庫很是有意思,是加強版的發佈訂閱模式
。發佈訂閱模式
在源碼世界實在是太常見了,咱們已經在多個庫源碼裏面見過了:javascript
這些庫基本都本身實現了本身的發佈訂閱模式
,實現方式主要是用來知足本身的業務需求,而tapable
並無具體的業務邏輯,是一個專門用來實現事件訂閱或者他本身稱爲hook
(鉤子)的工具庫,其根本原理仍是發佈訂閱模式
,可是他實現了多種形式的發佈訂閱模式
,還包含了多種形式的流程控制。前端
tapable
暴露多個API,提供了多種流程控制方式,連使用都是比較複雜的,因此我想分兩篇文章來寫他的原理:java
本文就是講用法的文章,知道了他的用法,你們之後若是有本身實現hook
或者事件監聽的需求,能夠直接拿過來用,很是強大!node
本文例子已經所有上傳到GitHub,你們能夠拿下來作個參考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usagewebpack
tapable
是webpack
的核心模塊,也是webpack
團隊維護的,是webpack plugin
的基本實現方式。他的主要功能是爲使用者提供強大的hook
機制,webpack plugin
就是基於hook
的。git
下面是官方文檔中列出來的主要API,全部API的名字都是以Hook
結尾的:github
const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
這些API的名字其實就解釋了他的做用,注意這些關鍵字:Sync
, Async
, Bail
, Waterfall
, Loop
, Parallel
, Series
。下面分別來解釋下這些關鍵字:web
Sync:這是一個同步的hook
編程
Async:這是一個異步的hook
redux
Bail:Bail
在英文中的意思是保險,保障
的意思,實現的效果是,當一個hook
註冊了多個回調方法,任意一個回調方法返回了不爲undefined
的值,就再也不執行後面的回調方法了,就起到了一個「保險絲」的做用。
Waterfall:Waterfall
在英語中是瀑布
的意思,在編程世界中表示順序執行各類任務,在這裏實現的效果是,當一個hook
註冊了多個回調方法,前一個回調執行完了纔會執行下一個回調,而前一個回調的執行結果會做爲參數傳給下一個回調函數。
Loop:Loop
就是循環的意思,實現的效果是,當一個hook
註冊了回調方法,若是這個回調方法返回了true
就重複循環這個回調,只有當這個回調返回undefined
才執行下一個回調。
Parallel:Parallel
是並行的意思,有點相似於Promise.all
,就是當一個hook
註冊了多個回調方法,這些回調同時開始並行執行。
Series:Series
就是串行的意思,就是當一個hook
註冊了多個回調方法,前一個執行完了纔會執行下一個。
Parallel
和Series
的概念只存在於異步的hook
中,由於同步hook
所有是串行的。
下面咱們分別來介紹下每一個API的用法和效果。
同步API就是這幾個:
const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, } = require("tapable");
前面說了,同步API所有是串行的,因此這幾個的區別就在流程控制上。
SyncHook
是一個最基礎的hook
,其使用方法和效果接近咱們常用的發佈訂閱模式
,注意tapable
導出的全部hook
都是類,基本用法是這樣的:
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
由於SyncHook
是一個類,因此使用new
來生成一個實例,構造函數接收的參數是一個數組["arg1", "arg2", "arg3"]
,這個數組有三項,表示生成的這個實例註冊回調的時候接收三個參數。實例hook
主要有兩個實例方法:
tap
:就是註冊事件回調的方法。call
:就是觸發事件,執行回調的方法。下面咱們擴展下官方文檔中小汽車加速的例子來講明下具體用法:
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.tap("DamagePlugin", (newSpeed) => { if (newSpeed > 300) { console.log("DamagePlugin", "速度實在太快,車子快散架了。。。"); } }); // 觸發一下加速事件,看看效果吧 accelerate.call(500);
而後運行下看看吧,當加速事件出現的時候,會依次執行這三個回調:
上面這個例子主要就是用了tap
和call
這兩個實例方法,其中tap
接收兩個參數,第一個是個字符串,並無實際用處,僅僅是一個註釋的做用,第二個參數就是一個回調函數,用來執行事件觸發時的具體邏輯。
accelerate.tap("LoggerPlugin", (newSpeed) => console.log("LoggerPlugin", `加速到${newSpeed}`) );
上述這種寫法其實與webpack官方文檔中對於plugin的介紹很是像了,由於webpack
的plguin
就是用tapable
實現的,第一個參數通常就是plugin
的名字:
而call
就是簡單的觸發這個事件,在webpack
的plguin
中通常不須要開發者去觸發事件,而是webpack
本身在不一樣階段會觸發不一樣的事件,好比beforeRun
, run
等等,plguin
開發者更多的會關注這些事件出現時應該進行什麼操做,也就是在這些事件上註冊本身的回調。
上面的SyncHook
其實就是一個簡單的發佈訂閱模式
,SyncBailHook
就是在這個基礎上加了一點流程控制,前面咱們說過了,Bail
就是個保險,實現的效果是,前面一個回調返回一個不爲undefined
的值,就中斷這個流程。好比咱們如今將前面這個例子的SyncHook
換成SyncBailHook
,而後在檢測超速的這個插件裏面加點邏輯,當它超速了就返回錯誤,後面的DamagePlugin
就不會執行了:
const { SyncBailHook } = require("tapable"); // 使用的是SyncBailHook // 實例化一個加速的hook 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('您已超速!!'); } }); accelerate.tap("DamagePlugin", (newSpeed) => { if (newSpeed > 300) { console.log("DamagePlugin", "速度實在太快,車子快散架了。。。"); } }); accelerate.call(500);
而後再運行下看看:
能夠看到因爲OverspeedPlugin
返回了一個不爲undefined
的值,DamagePlugin
被阻斷,沒有運行了。
SyncWaterfallHook
也是在SyncHook
的基礎上加了點流程控制,前面說了,Waterfall
實現的效果是將上一個回調的返回值做爲參數傳給下一個回調。因此經過call
傳入的參數只會傳遞給第一個回調函數,後面的回調接受都是上一個回調的返回值,最後一個回調的返回值會做爲call
的返回值返回給最外層:
const { SyncWaterfallHook } = require("tapable"); const accelerate = new SyncWaterfallHook(["newSpeed"]); accelerate.tap("LoggerPlugin", (newSpeed) => { console.log("LoggerPlugin", `加速到${newSpeed}`); return "LoggerPlugin"; }); accelerate.tap("Plugin2", (data) => { console.log(`上一個插件是: ${data}`); return "Plugin2"; }); accelerate.tap("Plugin3", (data) => { console.log(`上一個插件是: ${data}`); return "Plugin3"; }); const lastPlugin = accelerate.call(100); console.log(`最後一個插件是:${lastPlugin}`);
而後看下運行效果吧:
SyncLoopHook
是在SyncHook
的基礎上添加了循環的邏輯,也就是若是一個插件返回true
就會一直執行這個插件,直到他返回undefined
纔會執行下一個插件:
const { SyncLoopHook } = require("tapable"); const accelerate = new SyncLoopHook(["newSpeed"]); accelerate.tap("LoopPlugin", (newSpeed) => { console.log("LoopPlugin", `循環加速到${newSpeed}`); return new Date().getTime() % 5 !== 0 ? true : undefined; }); accelerate.tap("LastPlugin", (newSpeed) => { console.log("循環加速總算結束了"); }); accelerate.call(100);
執行效果以下:
所謂異步API是相對前面的同步API來講的,前面的同步API的全部回調都是按照順序同步執行的,每一個回調內部也所有是同步代碼。可是實際項目中,可能須要回調裏面處理異步狀況,也可能但願多個回調能夠同時並行執行,也就是Parallel
。這些需求就須要用到異步API了,主要的異步API就是這些:
const { AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
既然涉及到了異步,那確定還須要異步的處理方式,tapable
支持回調函數和Promise
兩種異步的處理方式。因此這些異步API除了用前面的tap
來註冊回調外,還有兩個註冊回調的方法:tapAsync
和tapPromise
,對應的觸發事件的方法爲callAsync
和promise
。下面分別來看下每一個API吧:
AsyncParallelHook
從前面介紹的命名規則能夠看出,他是一個異步並行執行的Hook
,咱們先用tapAsync
的方式來看下怎麼用吧。
仍是那個小汽車加速的例子,只不過這個小汽車加速沒那麼快了,須要一秒才能加速完成,而後咱們在2秒的時候分別檢測是否超速和是否損壞,爲了看出並行的效果,咱們記錄下整個過程從開始到結束的時間:
const { AsyncParallelHook } = require("tapable"); const accelerate = new AsyncParallelHook(["newSpeed"]); console.time("total time"); // 記錄起始時間 // 注意註冊異步事件須要使用tapAsync // 接收的最後一個參數是done,調用他來表示當前任務執行完畢 accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => { // 1秒後加速才完成 setTimeout(() => { console.log("LoggerPlugin", `加速到${newSpeed}`); done(); }, 1000); }); accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => { // 2秒後檢測是否超速 setTimeout(() => { if (newSpeed > 120) { console.log("OverspeedPlugin", "您已超速!!"); } done(); }, 2000); }); accelerate.tapAsync("DamagePlugin", (newSpeed, done) => { // 2秒後檢測是否損壞 setTimeout(() => { if (newSpeed > 300) { console.log("DamagePlugin", "速度實在太快,車子快散架了。。。"); } done(); }, 2000); }); accelerate.callAsync(500, () => { console.log("任務所有完成"); console.timeEnd("total time"); // 記錄總共耗時 });
上面代碼須要注意的是,註冊回調要使用tapAsync
,並且回調函數裏面最後一個參數會自動傳入done
,你能夠調用他來通知tapable
當前任務已經完成。觸發任務須要使用callAsync
,他最後也接收一個函數,能夠用來處理全部任務都完成後須要執行的操做。因此上面的運行結果就是:
從這個結果能夠看出,最終消耗的時間大概是2秒,也就是三個任務中最長的單個任務耗時,而不是三個任務耗時的總額,這就實現了Parallel
並行的效果。
如今都流行Promise
,因此tapable
也是支持的,執行效果是同樣的,只是寫法不同而已。要用tapPromise
,須要註冊的回調返回一個promise
,同時觸發事件也須要用promise
,任務運行完執行的處理能夠直接使用then
,因此上述代碼改成:
const { AsyncParallelHook } = require("tapable"); const accelerate = new AsyncParallelHook(["newSpeed"]); console.time("total time"); // 記錄起始時間 // 注意註冊異步事件須要使用tapPromise // 回調函數要返回一個promise accelerate.tapPromise("LoggerPlugin", (newSpeed) => { return new Promise((resolve) => { // 1秒後加速才完成 setTimeout(() => { console.log("LoggerPlugin", `加速到${newSpeed}`); resolve(); }, 1000); }); }); accelerate.tapPromise("OverspeedPlugin", (newSpeed) => { return new Promise((resolve) => { // 2秒後檢測是否超速 setTimeout(() => { if (newSpeed > 120) { console.log("OverspeedPlugin", "您已超速!!"); } resolve(); }, 2000); }); }); accelerate.tapPromise("DamagePlugin", (newSpeed) => { return new Promise((resolve) => { // 2秒後檢測是否損壞 setTimeout(() => { if (newSpeed > 300) { console.log("DamagePlugin", "速度實在太快,車子快散架了。。。"); } resolve(); }, 2000); }); }); // 觸發事件使用promise,直接用then處理最後的結果 accelerate.promise(500).then(() => { console.log("任務所有完成"); console.timeEnd("total time"); // 記錄總共耗時 });
這段代碼的邏輯和運行結果和上面那個是同樣的,只是寫法不同:
既然tapable
支持這兩種異步寫法,那這兩種寫法能夠混用嗎?咱們來試試吧:
const { AsyncParallelHook } = require("tapable"); const accelerate = new AsyncParallelHook(["newSpeed"]); console.time("total time"); // 記錄起始時間 // 來一個promise寫法 accelerate.tapPromise("LoggerPlugin", (newSpeed) => { return new Promise((resolve) => { // 1秒後加速才完成 setTimeout(() => { console.log("LoggerPlugin", `加速到${newSpeed}`); resolve(); }, 1000); }); }); // 再來一個async寫法 accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => { // 2秒後檢測是否超速 setTimeout(() => { if (newSpeed > 120) { console.log("OverspeedPlugin", "您已超速!!"); } done(); }, 2000); }); // 使用promise觸發事件 // accelerate.promise(500).then(() => { // console.log("任務所有完成"); // console.timeEnd("total time"); // 記錄總共耗時 // }); // 使用callAsync觸發事件 accelerate.callAsync(500, () => { console.log("任務所有完成"); console.timeEnd("total time"); // 記錄總共耗時 });
這段代碼不管我是使用promise
觸發事件仍是callAsync
觸發運行的結果都是同樣的,因此tapable
內部應該是作了兼容轉換的,兩種寫法能夠混用:
因爲tapAsync
和tapPromise
只是寫法上的不同,我後面的例子就所有用tapAsync
了。
前面已經看了SyncBailHook
,知道帶Bail
的功能就是當一個任務返回不爲undefined
的時候,阻斷後面任務的執行。可是因爲Parallel
任務都是同時開始的,阻斷是阻斷不了了,實際效果是若是有一個任務返回了不爲undefined
的值,最終的回調會當即執行,而且獲取Bail
任務的返回值。咱們將上面三個任務執行時間錯開,分別爲1秒,2秒,3秒,而後在2秒的任務觸發Bail
就能看到效果了:
const { AsyncParallelBailHook } = require("tapable"); const accelerate = new AsyncParallelBailHook(["newSpeed"]); console.time("total time"); // 記錄起始時間 accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => { // 1秒後加速才完成 setTimeout(() => { console.log("LoggerPlugin", `加速到${newSpeed}`); done(); }, 1000); }); accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => { // 2秒後檢測是否超速 setTimeout(() => { if (newSpeed > 120) { console.log("OverspeedPlugin", "您已超速!!"); } // 這個任務的done返回一個錯誤 // 注意第一個參數是node回調約定俗成的錯誤 // 第二個參數纔是Bail的返回值 done(null, new Error("您已超速!!")); }, 2000); }); accelerate.tapAsync("DamagePlugin", (newSpeed, done) => { // 3秒後檢測是否損壞 setTimeout(() => { if (newSpeed > 300) { console.log("DamagePlugin", "速度實在太快,車子快散架了。。。"); } done(); }, 3000); }); accelerate.callAsync(500, (error, data) => { if (data) { console.log("任務執行出錯:", data); } else { console.log("任務所有完成"); } console.timeEnd("total time"); // 記錄總共耗時 });
能夠看到執行到任務2時,因爲他返回了一個錯誤,因此最終的回調會當即執行,可是因爲任務3以前已經同步開始了,因此他本身仍然會運行完,只是已經不影響最終結果了:
AsyncSeriesHook
是異步串行hook
,若是有多個任務,這多個任務之間是串行的,可是任務自己卻多是異步的,下一個任務必須等上一個任務done
了才能開始:
const { AsyncSeriesHook } = require("tapable"); const accelerate = new AsyncSeriesHook(["newSpeed"]); console.time("total time"); // 記錄起始時間 accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => { // 1秒後加速才完成 setTimeout(() => { console.log("LoggerPlugin", `加速到${newSpeed}`); done(); }, 1000); }); accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => { // 2秒後檢測是否超速 setTimeout(() => { if (newSpeed > 120) { console.log("OverspeedPlugin", "您已超速!!"); } done(); }, 2000); }); accelerate.tapAsync("DamagePlugin", (newSpeed, done) => { // 2秒後檢測是否損壞 setTimeout(() => { if (newSpeed > 300) { console.log("DamagePlugin", "速度實在太快,車子快散架了。。。"); } done(); }, 2000); }); accelerate.callAsync(500, () => { console.log("任務所有完成"); console.timeEnd("total time"); // 記錄總共耗時 });
每一個任務代碼跟AsyncParallelHook
是同樣的,只是使用的Hook
不同,而最終效果的區別是:AsyncParallelHook
全部任務同時開始,因此最終總耗時就是耗時最長的那個任務的耗時;AsyncSeriesHook
的任務串行執行,下一個任務要等上一個任務完成了才能開始,因此最終總耗時是全部任務耗時的總和,上面這個例子就是1 + 2 + 2
,也就是5秒:
AsyncSeriesBailHook
就是在AsyncSeriesHook
的基礎上加上了Bail
的邏輯,也就是中間任何一個任務返回不爲undefined
的值,終止執行,直接執行最後的回調,而且將這個返回值傳給最終的回調:
const { AsyncSeriesBailHook } = require("tapable"); const accelerate = new AsyncSeriesBailHook(["newSpeed"]); console.time("total time"); // 記錄起始時間 accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => { // 1秒後加速才完成 setTimeout(() => { console.log("LoggerPlugin", `加速到${newSpeed}`); done(); }, 1000); }); accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => { // 2秒後檢測是否超速 setTimeout(() => { if (newSpeed > 120) { console.log("OverspeedPlugin", "您已超速!!"); } // 這個任務的done返回一個錯誤 // 注意第一個參數是node回調約定俗成的錯誤 // 第二個參數纔是Bail的返回值 done(null, new Error("您已超速!!")); }, 2000); }); accelerate.tapAsync("DamagePlugin", (newSpeed, done) => { // 2秒後檢測是否損壞 setTimeout(() => { if (newSpeed > 300) { console.log("DamagePlugin", "速度實在太快,車子快散架了。。。"); } done(); }, 2000); }); accelerate.callAsync(500, (error, data) => { if (data) { console.log("任務執行出錯:", data); } else { console.log("任務所有完成"); } console.timeEnd("total time"); // 記錄總共耗時 });
這個執行結果跟AsyncParallelBailHook
的區別就是AsyncSeriesBailHook
被阻斷後,後面的任務因爲還沒開始,因此能夠被徹底阻斷,而AsyncParallelBailHook
後面的任務因爲已經開始了,因此還會繼續執行,只是結果已經不關心了。
Waterfall
的做用是將前一個任務的結果傳給下一個任務,其餘的跟AsyncSeriesHook
同樣的,直接來看代碼吧:
const { AsyncSeriesWaterfallHook } = require("tapable"); const accelerate = new AsyncSeriesWaterfallHook(["newSpeed"]); console.time("total time"); // 記錄起始時間 accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => { // 1秒後加速才完成 setTimeout(() => { console.log("LoggerPlugin", `加速到${newSpeed}`); // 注意done的第一個參數會被當作error // 第二個參數纔是傳遞給後面任務的參數 done(null, "LoggerPlugin"); }, 1000); }); accelerate.tapAsync("Plugin2", (data, done) => { setTimeout(() => { console.log(`上一個插件是: ${data}`); done(null, "Plugin2"); }, 2000); }); accelerate.tapAsync("Plugin3", (data, done) => { setTimeout(() => { console.log(`上一個插件是: ${data}`); done(null, "Plugin3"); }, 2000); }); accelerate.callAsync(500, (error, data) => { console.log("最後一個插件是:", data); console.timeEnd("total time"); // 記錄總共耗時 });
運行效果以下:
本文例子已經所有上傳到GitHub,你們能夠拿下來作個參考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage
tapable
是webpack
實現plugin
的核心庫,他爲webpack
提供了多種事件處理和流程控制的Hook
。Hook
主要有同步(Sync
)和異步(Async
)兩種,同時還提供了阻斷(Bail
),瀑布(Waterfall
),循環(Loop
)等流程控制,對於異步流程還提供了並行(Paralle
)和串行(Series
)兩種控制方式。tapable
其核心原理仍是事件的發佈訂閱模式
,他使用tap
來註冊事件,使用call
來觸發事件。hook
支持兩種寫法:回調和Promise
,註冊和觸發事件分別使用tapAsync/callAsync
和tapPromise/promise
。hook
使用回調寫法的時候要注意,回調函數的第一個參數默認是錯誤,第二個參數纔是向外傳遞的數據,這也符合node
回調的風格。這篇文章主要講述了tapable
的用法,後面我會寫一篇文章來分析他的源碼,點個關注不迷路,哈哈~
文章的最後,感謝你花費寶貴的時間閱讀本文,若是本文給了你一點點幫助或者啓發,請不要吝嗇你的贊和GitHub小星星,你的支持是做者持續創做的動力。
歡迎關注個人公衆號進擊的大前端第一時間獲取高質量原創~
「前端進階知識」系列文章源碼地址: https://github.com/dennis-jiang/Front-End-Knowledges