天天看點

webpack核心子產品tapable用法解析

前不久寫了一篇webpack基本原理和AST用法的文章,本來想接着寫<code>webpack plugin</code>的原理的,但是發現<code>webpack plugin</code>高度依賴tapable這個庫,不清楚<code>tapable</code>而直接去看<code>webpack plugin</code>始終有點霧裡看花的意思。是以就先去看了下<code>tapable</code>的文檔和源碼,發現這個庫非常有意思,是增強版的<code>釋出訂閱模式</code>。<code>釋出訂閱模式</code>在源碼世界實在是太常見了,我們已經在多個庫源碼裡面見過了:

<code>redux</code>的<code>subscribe</code>和<code>dispatch</code>

<code>Node.js</code>的<code>EventEmitter</code>

<code>redux-saga</code>的<code>take</code>和<code>put</code>

這些庫基本都自己實作了自己的<code>釋出訂閱模式</code>,實作方式主要是用來滿足自己的業務需求,而<code>tapable</code>并沒有具體的業務邏輯,是一個專門用來實作事件訂閱或者他自己稱為<code>hook</code>(鈎子)的工具庫,其根本原理還是<code>釋出訂閱模式</code>,但是他實作了多種形式的<code>釋出訂閱模式</code>,還包含了多種形式的流程控制。

<code>tapable</code>暴露多個API,提供了多種流程控制方式,連使用都是比較複雜的,是以我想分兩篇文章來寫他的原理:

先看看用法,體驗下他的多種流程控制方式

通過用法去看看源碼是怎麼實作的

本文就是講用法的文章,知道了他的用法,大家以後如果有自己實作<code>hook</code>或者事件監聽的需求,可以直接拿過來用,非常強大!

本文例子已經全部上傳到GitHub,大家可以拿下來做個參考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage

<code>tapable</code>是<code>webpack</code>的核心子產品,也是<code>webpack</code>團隊維護的,是<code>webpack plugin</code>的基本實作方式。他的主要功能是為使用者提供強大的<code>hook</code>機制,<code>webpack plugin</code>就是基于<code>hook</code>的。

下面是官方文檔中列出來的主要API,所有API的名字都是以<code>Hook</code>結尾的:

這些API的名字其實就解釋了他的作用,注意這些關鍵字:<code>Sync</code>, <code>Async</code>, <code>Bail</code>, <code>Waterfall</code>, <code>Loop</code>, <code>Parallel</code>, <code>Series</code>。下面分别來解釋下這些關鍵字:

Sync:這是一個同步的<code>hook</code>

Async:這是一個異步的<code>hook</code>

Bail:<code>Bail</code>在英文中的意思是<code>保險,保障</code>的意思,實作的效果是,當一個<code>hook</code>注冊了多個回調方法,任意一個回調方法傳回了不為<code>undefined</code>的值,就不再執行後面的回調方法了,就起到了一個“保險絲”的作用。

Waterfall:<code>Waterfall</code>在英語中是<code>瀑布</code>的意思,在程式設計世界中表示順序執行各種任務,在這裡實作的效果是,當一個<code>hook</code>注冊了多個回調方法,前一個回調執行完了才會執行下一個回調,而前一個回調的執行結果會作為參數傳給下一個回調函數。

Loop:<code>Loop</code>就是循環的意思,實作的效果是,當一個<code>hook</code>注冊了回調方法,如果這個回調方法傳回了<code>true</code>就重複循環這個回調,隻有當這個回調傳回<code>undefined</code>才執行下一個回調。

Parallel:<code>Parallel</code>是并行的意思,有點類似于<code>Promise.all</code>,就是當一個<code>hook</code>注冊了多個回調方法,這些回調同時開始并行執行。

Series:<code>Series</code>就是串行的意思,就是當一個<code>hook</code>注冊了多個回調方法,前一個執行完了才會執行下一個。

<code>Parallel</code>和<code>Series</code>的概念隻存在于異步的<code>hook</code>中,因為同步<code>hook</code>全部是串行的。

下面我們分别來介紹下每個API的用法和效果。

同步API就是這幾個:

前面說了,同步API全部是串行的,是以這幾個的差別就在流程控制上。

<code>SyncHook</code>是一個最基礎的<code>hook</code>,其使用方法和效果接近我們經常使用的<code>釋出訂閱模式</code>,注意<code>tapable</code>導出的所有<code>hook</code>都是類,基本用法是這樣的:

因為<code>SyncHook</code>是一個類,是以使用<code>new</code>來生成一個執行個體,構造函數接收的參數是一個數組<code>["arg1", "arg2", "arg3"]</code>,這個數組有三項,表示生成的這個執行個體注冊回調的時候接收三個參數。執行個體<code>hook</code>主要有兩個執行個體方法:

<code>tap</code>:就是注冊事件回調的方法。

<code>call</code>:就是觸發事件,執行回調的方法。

下面我們擴充下官方文檔中小汽車加速的例子來說明下具體用法:

然後運作下看看吧,當加速事件出現的時候,會依次執行這三個回調:

webpack核心子產品tapable用法解析

上面這個例子主要就是用了<code>tap</code>和<code>call</code>這兩個執行個體方法,其中<code>tap</code>接收兩個參數,第一個是個字元串,并沒有實際用處,僅僅是一個注釋的作用,第二個參數就是一個回調函數,用來執行事件觸發時的具體邏輯。

上述這種寫法其實與webpack官方文檔中對于plugin的介紹非常像了,因為<code>webpack</code>的<code>plguin</code>就是用<code>tapable</code>實作的,第一個參數一般就是<code>plugin</code>的名字:

webpack核心子產品tapable用法解析

而<code>call</code>就是簡單的觸發這個事件,在<code>webpack</code>的<code>plguin</code>中一般不需要開發者去觸發事件,而是<code>webpack</code>自己在不同階段會觸發不同的事件,比如<code>beforeRun</code>, <code>run</code>等等,<code>plguin</code>開發者更多的會關注這些事件出現時應該進行什麼操作,也就是在這些事件上注冊自己的回調。

上面的<code>SyncHook</code>其實就是一個簡單的<code>釋出訂閱模式</code>,<code>SyncBailHook</code>就是在這個基礎上加了一點流程控制,前面我們說過了,<code>Bail</code>就是個保險,實作的效果是,前面一個回調傳回一個不為<code>undefined</code>的值,就中斷這個流程。比如我們現在将前面這個例子的<code>SyncHook</code>換成<code>SyncBailHook</code>,然後在檢測超速的這個插件裡面加點邏輯,當它超速了就傳回錯誤,後面的<code>DamagePlugin</code>就不會執行了:

然後再運作下看看:

webpack核心子產品tapable用法解析

可以看到由于<code>OverspeedPlugin</code>傳回了一個不為<code>undefined</code>的值,<code>DamagePlugin</code>被阻斷,沒有運作了。

<code>SyncWaterfallHook</code>也是在<code>SyncHook</code>的基礎上加了點流程控制,前面說了,<code>Waterfall</code>實作的效果是将上一個回調的傳回值作為參數傳給下一個回調。是以通過<code>call</code>傳入的參數隻會傳遞給第一個回調函數,後面的回調接受都是上一個回調的傳回值,最後一個回調的傳回值會作為<code>call</code>的傳回值傳回給最外層:

然後看下運作效果吧:

webpack核心子產品tapable用法解析

<code>SyncLoopHook</code>是在<code>SyncHook</code>的基礎上添加了循環的邏輯,也就是如果一個插件傳回<code>true</code>就會一直執行這個插件,直到他傳回<code>undefined</code>才會執行下一個插件:

執行效果如下:

webpack核心子產品tapable用法解析

所謂異步API是相對前面的同步API來說的,前面的同步API的所有回調都是按照順序同步執行的,每個回調内部也全部是同步代碼。但是實際項目中,可能需要回調裡面處理異步情況,也可能希望多個回調可以同時并行執行,也就是<code>Parallel</code>。這些需求就需要用到異步API了,主要的異步API就是這些:

既然涉及到了異步,那肯定還需要異步的處理方式,<code>tapable</code>支援回調函數和<code>Promise</code>兩種異步的處理方式。是以這些異步API除了用前面的<code>tap</code>來注冊回調外,還有兩個注冊回調的方法:<code>tapAsync</code>和<code>tapPromise</code>,對應的觸發事件的方法為<code>callAsync</code>和<code>promise</code>。下面分别來看下每個API吧:

<code>AsyncParallelHook</code>從前面介紹的命名規則可以看出,他是一個異步并行執行的<code>Hook</code>,我們先用<code>tapAsync</code>的方式來看下怎麼用吧。

還是那個小汽車加速的例子,隻不過這個小汽車加速沒那麼快了,需要一秒才能加速完成,然後我們在2秒的時候分别檢測是否超速和是否損壞,為了看出并行的效果,我們記錄下整個過程從開始到結束的時間:

上面代碼需要注意的是,注冊回調要使用<code>tapAsync</code>,而且回調函數裡面最後一個參數會自動傳入<code>done</code>,你可以調用他來通知<code>tapable</code>目前任務已經完成。觸發任務需要使用<code>callAsync</code>,他最後也接收一個函數,可以用來處理所有任務都完成後需要執行的操作。是以上面的運作結果就是:

webpack核心子產品tapable用法解析

從這個結果可以看出,最終消耗的時間大概是2秒,也就是三個任務中最長的單個任務耗時,而不是三個任務耗時的總額,這就實作了<code>Parallel</code>并行的效果。

現在都流行<code>Promise</code>,是以<code>tapable</code>也是支援的,執行效果是一樣的,隻是寫法不一樣而已。要用<code>tapPromise</code>,需要注冊的回調傳回一個<code>promise</code>,同時觸發事件也需要用<code>promise</code>,任務運作完執行的處理可以直接使用<code>then</code>,是以上述代碼改為:

這段代碼的邏輯和運作結果和上面那個是一樣的,隻是寫法不一樣:

webpack核心子產品tapable用法解析

既然<code>tapable</code>支援這兩種異步寫法,那這兩種寫法可以混用嗎?我們來試試吧:

這段代碼無論我是使用<code>promise</code>觸發事件還是<code>callAsync</code>觸發運作的結果都是一樣的,是以<code>tapable</code>内部應該是做了相容轉換的,兩種寫法可以混用:

webpack核心子產品tapable用法解析

由于<code>tapAsync</code>和<code>tapPromise</code>隻是寫法上的不一樣,我後面的例子就全部用<code>tapAsync</code>了。

前面已經看了<code>SyncBailHook</code>,知道帶<code>Bail</code>的功能就是當一個任務傳回不為<code>undefined</code>的時候,阻斷後面任務的執行。但是由于<code>Parallel</code>任務都是同時開始的,阻斷是阻斷不了了,實際效果是如果有一個任務傳回了不為<code>undefined</code>的值,最終的回調會立即執行,并且擷取<code>Bail</code>任務的傳回值。我們将上面三個任務執行時間錯開,分别為1秒,2秒,3秒,然後在2秒的任務觸發<code>Bail</code>就能看到效果了:

可以看到執行到任務2時,由于他傳回了一個錯誤,是以最終的回調會立即執行,但是由于任務3之前已經同步開始了,是以他自己仍然會運作完,隻是已經不影響最終結果了:

webpack核心子產品tapable用法解析

<code>AsyncSeriesHook</code>是異步串行<code>hook</code>,如果有多個任務,這多個任務之間是串行的,但是任務本身卻可能是異步的,下一個任務必須等上一個任務<code>done</code>了才能開始:

每個任務代碼跟<code>AsyncParallelHook</code>是一樣的,隻是使用的<code>Hook</code>不一樣,而最終效果的差別是:<code>AsyncParallelHook</code>所有任務同時開始,是以最終總耗時就是耗時最長的那個任務的耗時;<code>AsyncSeriesHook</code>的任務串行執行,下一個任務要等上一個任務完成了才能開始,是以最終總耗時是所有任務耗時的總和,上面這個例子就是<code>1 + 2 + 2</code>,也就是5秒:

webpack核心子產品tapable用法解析

<code>AsyncSeriesBailHook</code>就是在<code>AsyncSeriesHook</code>的基礎上加上了<code>Bail</code>的邏輯,也就是中間任何一個任務傳回不為<code>undefined</code>的值,終止執行,直接執行最後的回調,并且将這個傳回值傳給最終的回調:

這個執行結果跟<code>AsyncParallelBailHook</code>的差別就是<code>AsyncSeriesBailHook</code>被阻斷後,後面的任務由于還沒開始,是以可以被完全阻斷,而<code>AsyncParallelBailHook</code>後面的任務由于已經開始了,是以還會繼續執行,隻是結果已經不關心了。

webpack核心子產品tapable用法解析

<code>Waterfall</code>的作用是将前一個任務的結果傳給下一個任務,其他的跟<code>AsyncSeriesHook</code>一樣的,直接來看代碼吧:

運作效果如下:

webpack核心子產品tapable用法解析

<code>tapable</code>是<code>webpack</code>實作<code>plugin</code>的核心庫,他為<code>webpack</code>提供了多種事件處理和流程控制的<code>Hook</code>。

這些<code>Hook</code>主要有同步(<code>Sync</code>)和異步(<code>Async</code>)兩種,同時還提供了阻斷(<code>Bail</code>),瀑布(<code>Waterfall</code>),循環(<code>Loop</code>)等流程控制,對于異步流程還提供了并行(<code>Paralle</code>)和串行(<code>Series</code>)兩種控制方式。

<code>tapable</code>其核心原理還是事件的<code>釋出訂閱模式</code>,他使用<code>tap</code>來注冊事件,使用<code>call</code>來觸發事件。

異步<code>hook</code>支援兩種寫法:回調和<code>Promise</code>,注冊和觸發事件分别使用<code>tapAsync/callAsync</code>和<code>tapPromise/promise</code>。

異步<code>hook</code>使用回調寫法的時候要注意,回調函數的第一個參數預設是錯誤,第二個參數才是向外傳遞的資料,這也符合<code>node</code>回調的風格。

文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝啬你的贊和GitHub小星星,你的支援是作者持續創作的動力。

歡迎關注我的公衆号進擊的大前端第一時間擷取高品質原創~

“前端進階知識”系列文章:https://juejin.im/post/5e3ffc85518825494e2772fd

“前端進階知識”系列文章源碼GitHub位址: https://github.com/dennis-jiang/Front-End-Knowledges

webpack核心子產品tapable用法解析

繼續閱讀