1. 程式人生 > >webpack核心模組tapable用法解析

webpack核心模組tapable用法解析

前不久寫了一篇[webpack基本原理和AST用法的文章](https://www.cnblogs.com/dennisj/p/14416726.html),本來想接著寫`webpack plugin`的原理的,但是發現`webpack plugin`高度依賴[tapable](https://github.com/webpack/tapable)這個庫,不清楚`tapable`而直接去看`webpack plugin`始終有點霧裡看花的意思。所以就先去看了下`tapable`的文件和原始碼,發現這個庫非常有意思,是增強版的`釋出訂閱模式`。`釋出訂閱模式`在原始碼世界實在是太常見了,我們已經在多個庫原始碼裡面見過了: 1. [`redux`的`subscribe`和`dispatch`](https://www.cnblogs.com/dennisj/p/13230081.html) 2. [`Node.js`的`EventEmitter`](https://www.cnblogs.com/dennisj/p/12559029.html) 3. [`redux-saga`的`take`和`put`](https://www.cnblogs.com/dennisj/p/13840442.html) 這些庫基本都自己實現了自己的`釋出訂閱模式`,實現方式主要是用來滿足自己的業務需求,而`tapable`並沒有具體的業務邏輯,是一個專門用來實現事件訂閱或者他自己稱為`hook`(鉤子)的工具庫,其根本原理還是`釋出訂閱模式`,但是他實現了多種形式的`釋出訂閱模式`,還包含了多種形式的流程控制。 `tapable`暴露多個API,提供了多種流程控制方式,連使用都是比較複雜的,所以我想分兩篇文章來寫他的原理: 1. 先看看用法,體驗下他的多種流程控制方式 2. 通過用法去看看原始碼是怎麼實現的 本文就是講用法的文章,知道了他的用法,大家以後如果有自己實現`hook`或者事件監聽的需求,可以直接拿過來用,非常強大! **本文例子已經全部上傳到GitHub,大家可以拿下來做個參考:[https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage](https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage)** ## tapable是什麼 `tapable`是`webpack`的核心模組,也是`webpack`團隊維護的,是`webpack plugin`的基本實現方式。他的主要功能是為使用者提供強大的`hook`機制,`webpack plugin`就是基於`hook`的。 ### 主要API 下面是官方文件中列出來的主要API,所有API的名字都是以`Hook`結尾的: ```javascript const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable"); ``` 這些API的名字其實就解釋了他的作用,注意這些關鍵字:`Sync`, `Async`, `Bail`, `Waterfall`, `Loop`, `Parallel`, `Series`。下面分別來解釋下這些關鍵字: **Sync**:這是一個同步的`hook` **Async**:這是一個非同步的`hook` **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 同步API就是這幾個: ```javascript const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, } = require("tapable"); ``` 前面說了,同步API全部是序列的,所以這幾個的區別就在流程控制上。 ### SyncHook `SyncHook`是一個最基礎的`hook`,其使用方法和效果接近我們經常使用的`釋出訂閱模式`,注意`tapable`匯出的所有`hook`都是類,基本用法是這樣的: ```javascript const hook = new SyncHook(["arg1", "arg2", "arg3"]); ``` 因為`SyncHook`是一個類,所以使用`new`來生成一個例項,建構函式接收的引數是一個數組`["arg1", "arg2", "arg3"]`,這個陣列有三項,表示生成的這個例項註冊回撥的時候接收三個引數。例項`hook`主要有兩個例項方法: 1. `tap`:就是註冊事件回撥的方法。 2. `call`:就是觸發事件,執行回撥的方法。 下面我們擴充套件下官方文件中小汽車加速的例子來說明下具體用法: ```javascript 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); ``` 然後執行下看看吧,當加速事件出現的時候,會依次執行這三個回撥: ![image-20210309160302799](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a2283716dcd4425bb64e91f3a46f6243~tplv-k3u1fbpfcp-zoom-1.image) 上面這個例子主要就是用了`tap`和`call`這兩個例項方法,其中`tap`接收兩個引數,第一個是個字串,並沒有實際用處,僅僅是一個註釋的作用,第二個引數就是一個回撥函式,用來執行事件觸發時的具體邏輯。 ```javascript accelerate.tap("LoggerPlugin", (newSpeed) => console.log("LoggerPlugin", `加速到${newSpeed}`) ); ``` 上述這種寫法其實與[webpack官方文件中對於plugin的介紹非常像了](https://www.webpackjs.com/concepts/plugins/#%E5%89%96%E6%9E%90),因為`webpack`的`plguin`就是用`tapable`實現的,第一個引數一般就是`plugin`的名字: ![image-20210309154641835](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2f5f64d4b1fc4b1ba2d36fa90b962d1c~tplv-k3u1fbpfcp-zoom-1.image) 而`call`就是簡單的觸發這個事件,在`webpack`的`plguin`中一般不需要開發者去觸發事件,而是`webpack`自己在不同階段會觸發不同的事件,比如`beforeRun`, `run`等等,`plguin`開發者更多的會關注這些事件出現時應該進行什麼操作,也就是在這些事件上註冊自己的回撥。 ### SyncBailHook 上面的`SyncHook`其實就是一個簡單的`釋出訂閱模式`,`SyncBailHook`就是在這個基礎上加了一點流程控制,前面我們說過了,`Bail`就是個保險,實現的效果是,前面一個回撥返回一個不為`undefined`的值,就中斷這個流程。比如我們現在將前面這個例子的`SyncHook`換成`SyncBailHook`,然後在檢測超速的這個外掛裡面加點邏輯,當它超速了就返回錯誤,後面的`DamagePlugin`就不會執行了: ```javascript 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); ``` 然後再執行下看看: ![image-20210309161001682](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f250ead3f23a48c79fe85e715d67ce9f~tplv-k3u1fbpfcp-zoom-1.image) 可以看到由於`OverspeedPlugin`返回了一個不為`undefined`的值,`DamagePlugin`被阻斷,沒有運行了。 ### SyncWaterfallHook `SyncWaterfallHook`也是在`SyncHook`的基礎上加了點流程控制,前面說了,`Waterfall`實現的效果是將上一個回撥的返回值作為引數傳給下一個回撥。所以通過`call`傳入的引數只會傳遞給第一個回撥函式,後面的回撥接受都是上一個回撥的返回值,最後一個回撥的返回值會作為`call`的返回值返回給最外層: ```javascript 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}`); ``` 然後看下執行效果吧: ![image-20210309162008465](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/424f3956f531496ca2d0cb4b8cd9c5bf~tplv-k3u1fbpfcp-zoom-1.image) ### SyncLoopHook `SyncLoopHook`是在`SyncHook`的基礎上添加了迴圈的邏輯,也就是如果一個外掛返回`true`就會一直執行這個外掛,直到他返回`undefined`才會執行下一個外掛: ```javascript 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); ``` 執行效果如下: ![image-20210309163514680](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ea6e4c764c714026b4d61c790ea26e4d~tplv-k3u1fbpfcp-zoom-1.image) ## 非同步API 所謂非同步API是相對前面的同步API來說的,前面的同步API的所有回撥都是按照順序同步執行的,每個回撥內部也全部是同步程式碼。但是實際專案中,可能需要回調裡面處理非同步情況,也可能希望多個回撥可以同時並行執行,也就是`Parallel`。這些需求就需要用到非同步API了,主要的非同步API就是這些: ```javascript const { AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable"); ``` 既然涉及到了非同步,那肯定還需要非同步的處理方式,`tapable`支援回撥函式和`Promise`兩種非同步的處理方式。所以這些非同步API除了用前面的`tap`來註冊回撥外,還有兩個註冊回撥的方法:`tapAsync`和`tapPromise`,對應的觸發事件的方法為`callAsync`和`promise`。下面分別來看下每個API吧: ### AsyncParallelHook `AsyncParallelHook`從前面介紹的命名規則可以看出,他是一個非同步並行執行的`Hook`,我們先用`tapAsync`的方式來看下怎麼用吧。 #### tapAsync和callAsync 還是那個小汽車加速的例子,只不過這個小汽車加速沒那麼快了,需要一秒才能加速完成,然後我們在2秒的時候分別檢測是否超速和是否損壞,為了看出並行的效果,我們記錄下整個過程從開始到結束的時間: ```javascript 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`,他最後也接收一個函式,可以用來處理所有任務都完成後需要執行的操作。所以上面的執行結果就是: ![image-20210309171527773](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/747ee1c798074c3da506dfc7ba8bde6e~tplv-k3u1fbpfcp-zoom-1.image) 從這個結果可以看出,最終消耗的時間大概是2秒,也就是三個任務中最長的單個任務耗時,而不是三個任務耗時的總額,這就實現了`Parallel`並行的效果。 #### tapPromise和promise 現在都流行`Promise`,所以`tapable`也是支援的,執行效果是一樣的,只是寫法不一樣而已。要用`tapPromise`,需要註冊的回撥返回一個`promise`,同時觸發事件也需要用`promise`,任務執行完執行的處理可以直接使用`then`,所以上述程式碼改為: ```javascript 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"); // 記錄總共耗時 }); ``` 這段程式碼的邏輯和執行結果和上面那個是一樣的,只是寫法不一樣: ![image-20210309172537951](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0d66d9f070074fb89a386c9e230729ca~tplv-k3u1fbpfcp-zoom-1.image) #### tapAsync和tapPromise混用 既然`tapable`支援這兩種非同步寫法,那這兩種寫法可以混用嗎?我們來試試吧: ```javascript 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`內部應該是做了相容轉換的,兩種寫法可以混用: ![image-20210309173217034](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3827315f75cb404ca54fd7e47f4dd3dc~tplv-k3u1fbpfcp-zoom-1.image) 由於`tapAsync`和`tapPromise`只是寫法上的不一樣,我後面的例子就全部用`tapAsync`了。 ### AsyncParallelBailHook 前面已經看了`SyncBailHook`,知道帶`Bail`的功能就是當一個任務返回不為`undefined`的時候,阻斷後面任務的執行。但是由於`Parallel`任務都是同時開始的,阻斷是阻斷不了了,實際效果是如果有一個任務返回了不為`undefined`的值,最終的回撥會立即執行,並且獲取`Bail`任務的返回值。我們將上面三個任務執行時間錯開,分別為1秒,2秒,3秒,然後在2秒的任務觸發`Bail`就能看到效果了: ```javascript 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之前已經同步開始了,所以他自己仍然會執行完,只是已經不影響最終結果了: ![image-20210311142451224](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/18f08d87c5484a29adff85eac2474104~tplv-k3u1fbpfcp-zoom-1.image) ### AsyncSeriesHook `AsyncSeriesHook`是非同步序列`hook`,如果有多個任務,這多個任務之間是序列的,但是任務本身卻可能是非同步的,下一個任務必須等上一個任務`done`了才能開始: ```javascript 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秒: ![image-20210311144738884](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6535d7cfd6a343aa9cfe9a4ab74ab3f7~tplv-k3u1fbpfcp-zoom-1.image) ### AsyncSeriesBailHook `AsyncSeriesBailHook`就是在`AsyncSeriesHook`的基礎上加上了`Bail`的邏輯,也就是中間任何一個任務返回不為`undefined`的值,終止執行,直接執行最後的回撥,並且將這個返回值傳給最終的回撥: ```javascript 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`後面的任務由於已經開始了,所以還會繼續執行,只是結果已經不關心了。 ![image-20210311145241190](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9e0877ba1746494dadfa4ccedcd7c008~tplv-k3u1fbpfcp-zoom-1.image) ### AsyncSeriesWaterfallHook `Waterfall`的作用是將前一個任務的結果傳給下一個任務,其他的跟`AsyncSeriesHook`一樣的,直接來看程式碼吧: ```javascript 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"); // 記錄總共耗時 }); ``` 執行效果如下: ![image-20210311150510851](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5ebc248997df45ac9c2126887c9f1c00~tplv-k3u1fbpfcp-zoom-1.image) ## 總結 **本文例子已經全部上傳到GitHub,大家可以拿下來做個參考:[https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage](https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage)** 1. `tapable`是`webpack`實現`plugin`的核心庫,他為`webpack`提供了多種事件處理和流程控制的`Hook`。 2. 這些`Hook`主要有同步(`Sync`)和非同步(`Async`)兩種,同時還提供了阻斷(`Bail`),瀑布(`Waterfall`),迴圈(`Loop`)等流程控制,對於非同步流程還提供了並行(`Paralle`)和序列(`Series`)兩種控制方式。 3. `tapable`其核心原理還是事件的`釋出訂閱模式`,他使用`tap`來註冊事件,使用`call`來觸發事件。 4. 非同步`hook`支援兩種寫法:回撥和`Promise`,註冊和觸發事件分別使用`tapAsync/callAsync`和`tapPromise/promise`。 5. 非同步`hook`使用回撥寫法的時候要注意,回撥函式的第一個引數預設是錯誤,第二個引數才是向外傳遞的資料,這也符合`node`回撥的風格。 **文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。** **歡迎關注我的公眾號[進擊的大前端](https://test-dennis.oss-cn-hangzhou.aliyuncs.com/QRCode/QR430.jpg)第一時間獲取高質量原創~** **“前端進階知識”系列文章:[https://juejin.im/post/5e3ffc85518825494e2772fd](https://juejin.im/post/5e3ffc85518825494e2772fd)** **“前端進階知識”系列文章原始碼GitHub地址: [https://github.com/dennis-jiang/Front-End-Knowledges](https://github.com/dennis-jiang/Front-End-Knowledges)** ![QR1270](https://test-dennis.oss-cn-hangzhou.aliyuncs.com/QRCode/QR1