一文詳解Webpack中Tapable事件機制
引言
Webpack
是前端工程化常用的靜態(tài)模塊打包工具。在合適的時機通過 Webpack
提供的 API 改變輸出結(jié)果,使 Webpack
可以執(zhí)行更廣泛的任務,擁有更強的構(gòu)建能力。
Webpack
的插件機制本質(zhì)上是一種事件流的機制,它的工作流程就是將各個插件串聯(lián)起來,而實現(xiàn)這一切的核心就是Tapable
,Webpack
中最核心的負責編譯的Compiler
和負責創(chuàng)建bundles的Compilation
都是Tapable
的實例。
本文將介紹 Tapable
的基本使用以及底層實現(xiàn)。
Tapable
Tapable
是一個類似于 Node.js
中的 EventEmitter
的庫,但它更專注于自定義事件的觸發(fā)和處理。通過 Tapable
我們可以注冊自定義事件,然后在適當?shù)臅r機去執(zhí)行自定義事件。這個和我們所熟知的生命周期函數(shù)類似,在特定的時機去觸發(fā)。
我們先看一個 簡單 Tapable
的 例子:
const { SyncHook } = require("tapable"); // 實例化 鉤子函數(shù) 定義形參 const syncHook = new SyncHook(["name"]); //通過tap函數(shù)注冊事件 syncHook.tap("同步鉤子1", (name) => { console.log("同步鉤子1", name); }); //同步鉤子 通過call 發(fā)布事件 syncHook.call("古茗前端");
通過上面的例子,我們大致可以將 Tapable
的使用分為以下三步:
- 實例化鉤子函數(shù)
- 事件注冊
- 事件觸發(fā)
事件注冊
- 同步的鉤子要用
tap
方法來注冊事件 - 異步的鉤子可以像同步方式一樣用
tap
方法來注冊,也可以用tapAsync
或tapPromise
異步方法來注冊。tapAsync
: 使用用tapAsync
方法來注冊hook
時,必須調(diào)用callback 回調(diào)函數(shù)。tapPromise
:使用tapPromise
方法來注冊hook
時,必須返回一個pormise
,異步任務完成后resolve
。
事件觸發(fā)
- 同步的鉤子要用
call
方法來觸發(fā) - 異步的鉤子需要用
callAsync
或promise
異步方法來觸發(fā)。callAsync
:當我們用callAsync
方法來調(diào)用hook
時,第二個參數(shù)是一個回調(diào)函數(shù),回調(diào)函數(shù)的參數(shù)是執(zhí)行任務的最后一個返回值promise
:當我們用promise
方法來調(diào)用hook
時,需要使用then
來處理執(zhí)行結(jié)果,參數(shù)是執(zhí)行任務的最后一個返回值。
Tapable Hook 鉤子
tapable 內(nèi)置了 9 種 hook 。分為 同步
、異步
兩種執(zhí)行方式。異步執(zhí)行 Hook 又可以分為 串行
執(zhí)行和 并行
執(zhí)行。除此之外,hook 可以根據(jù)執(zhí)行機制分為 常規(guī)
瀑布模式
熔斷模式
循環(huán)模式
四種執(zhí)行機制。
同步鉤子
同步鉤子顧名思義:同步執(zhí)行,上一個鉤子執(zhí)行完才會執(zhí)行下一個鉤子。
示例代碼如下:
const { SyncHook } = require("tapable"); // 實例化 鉤子函數(shù) 定義形參 const syncHook = new SyncHook(["name"]); //通過tap函數(shù)注冊事件 syncHook.tap("同步鉤子1", (name) => { console.log("同步鉤子1", name); }); //該監(jiān)聽函數(shù)有返回值 syncHook.tap("同步鉤子2", (name) => { console.log("同步鉤子2", name); }); //同步鉤子 通過call 發(fā)布事件 syncHook.call("古茗前端");
執(zhí)行結(jié)果如下所示:
異步鉤子
異步鉤子分為: 串行執(zhí)行和并行執(zhí)行。在串行執(zhí)行中,如果上一個鉤子沒有調(diào)用callback 回調(diào)函數(shù),下一個鉤子將不會觸發(fā)對應的事件監(jiān)聽。
示例代碼如下:
const { AsyncParallelHook, AsyncSeriesHook } = require("tapable"); const asyncParallelHook = new AsyncParallelHook(["name"]); const asyncSeriesHook = new AsyncSeriesHook(["name"]); //通過tap函數(shù)注冊事件 asyncParallelHook.tapAsync("異步并行鉤子1", (name, callback) => { setTimeout(() => { console.log("異步并行鉤子1", name); }, 3000); }); //該監(jiān)聽函數(shù)有返回值 asyncParallelHook.tapAsync("異步并行鉤子2", (name, callback) => { setTimeout(() => { console.log("異步并行鉤子2", name); }, 1500); }); //通過tap函數(shù)注冊事件 asyncSeriesHook.tapAsync("異步串行鉤子1", (name, callback) => { setTimeout(() => { console.log("異步串行鉤子1", name); }, 3000); }); //該監(jiān)聽函數(shù)有返回值 asyncSeriesHook.tapAsync("異步串行鉤子2", (name, callback) => { setTimeout(() => { console.log("異步串行鉤子2", name); }, 1500); }); // 異步并行鉤子 通過 callAsync 發(fā)布事件 asyncParallelHook.callAsync("古茗前端", () => { console.log("1111"); return "1122"; }); // 異步串行鉤子 通過 callAsync 發(fā)布事件 asyncSeriesHook.callAsync("古茗前端", () => { console.log("1111"); return "1122"; });
控制臺輸出結(jié)果如下圖所示:
串行鉤子1沒有調(diào)用callback, 所以串行鉤子2沒有觸發(fā)。添加callback后,控制臺輸出結(jié)果:
熔斷類
AsyncSeriesBailHook
是一個異步串行、熔斷類型的Hook
。在串行的執(zhí)行過程中,只要其中一個有返回值,后面的就不會執(zhí)行了。
示例代碼如下:
const { SyncBailHook, AsyncSeriesBailHook } = require("tapable"); const syncBailHook = new SyncBailHook(["name"]); const asyncSeriesBailHook = new AsyncSeriesBailHook(["name"]); syncBailHook.tap("保險類同步鉤子1", (name) => { console.log("保險類同步鉤子1", name); }); syncBailHook.tap("保險類同步鉤子2", (name) => { console.log("保險類同步鉤子2", name); return "有返回值"; }); syncBailHook.tap("保險類同步鉤子3", (name) => { console.log("保險類同步鉤子3", name); }); asyncSeriesBailHook.tapAsync("保險類異步串行鉤子1", (name, callback) => { setTimeout(() => { console.log("保險類異步串行鉤子1", name); callback(); }, 3000); }); asyncSeriesBailHook.tapAsync("保險類2步串行鉤子1", (name, callback) => { setTimeout(() => { console.log("保險類異步串行鉤子2", name); callback("有返回值"); }, 2000); }); asyncSeriesBailHook.tapAsync("保險類異步串行鉤子3", (name) => { setTimeout(() => { console.log("保險類異步串行鉤子3", name); }, 1000); }); syncBailHook.call("古茗前端"); asyncSeriesBailHook.callAsync("古茗前端", (result) => { console.log("result", result); });
控制臺輸出結(jié)果如下圖所示:
循環(huán)類
SyncLoopHook
是一個同步、循環(huán)類型的 Hook
。循環(huán)類型的含義是不停的循環(huán)執(zhí)行事件函數(shù),直到所有函數(shù)結(jié)果 result === undefined
,不符合條件就調(diào)頭重新開始執(zhí)行。
示例代碼:
const { SyncLoopHook } = require("tapable"); const syncLoopHook = new SyncLoopHook(["name"]); let count = 4; syncLoopHook.tap("循環(huán)鉤子1", (name) => { console.log("循環(huán)鉤子1", count); return count <= 3 ? undefined : count--; }); syncLoopHook.tap("循環(huán)鉤子2", (name) => { console.log("循環(huán)鉤子2", count); return count <= 2 ? undefined : count--; }); syncLoopHook.tap("循環(huán)鉤子3", (name) => { console.log("循環(huán)鉤子3", count); return count <= 1 ? undefined : count--; }); syncLoopHook.call();
控制臺輸出結(jié)果:
瀑布類
AsyncSeriesWaterfallHook
是一個異步串行、瀑布類型的 Hook
。如果前一個事件函數(shù)的結(jié)果 result !== undefined
,則 result
會作為后一個事件函數(shù)的第一個參數(shù)(也就是上一個函數(shù)的執(zhí)行結(jié)果會成為下一個函數(shù)的參數(shù))。
示例代碼:
const { AsyncSeriesWaterfallHook, SyncWaterfallHook } = require("tapable"); const syncWaterfallHook = new SyncWaterfallHook(["name"]); const asyncSeriesWaterfallHook = new AsyncSeriesWaterfallHook(["name"]); syncWaterfallHook.tap("瀑布式同步鉤子1", (name) => { console.log("瀑布式同步鉤子1", name); return "古茗前端1"; }); syncWaterfallHook.tap("瀑布式同步鉤子2", (name) => { console.log("瀑布式同步鉤子2", name); }); syncWaterfallHook.tap("瀑布式同步鉤子3", (name) => { console.log("瀑布式同步鉤子3", name); return "古茗前端3"; }); asyncSeriesWaterfallHook.tapAsync("瀑布式異步串行鉤子1", (name, callback) => { setTimeout(() => { console.log("瀑布式異步串行鉤子1", name); callback(); }, 1000); }); asyncSeriesWaterfallHook.tapAsync("瀑布式異步串行鉤子2", (name, callback) => { console.log("瀑布式異步串行鉤子2", name); setTimeout(() => { callback(); }, 2000); }); asyncSeriesWaterfallHook.tapAsync("瀑布式異步串行鉤子3", (name, callback) => { console.log("瀑布式異步串行鉤子3", name); setTimeout(() => { callback("古茗前端3"); }, 3000); }); syncWaterfallHook.call("古茗前端"); asyncSeriesWaterfallHook.callAsync("古茗前端", (result) => { console.log("result", result); });
控制臺輸出結(jié)果:
Tapable 高級特性
Intercept
除了通常的 tap/call
之外,所有 hook
鉤子都提供額外的攔截API。— intercept
接口。
intercept
支持的中間件如下圖所示:
intercept | 類型 | 描述 |
---|---|---|
call | (...args) => void | 當鉤子被觸發(fā)時,向攔截器添加調(diào)用將被觸發(fā)。您可以訪問hooks參數(shù) |
tap | (tap: Tap) | 將tap添加到攔截器將在插件點擊鉤子時觸發(fā)。提供的是Tap對象。無法更改Tap對象 |
loop | (...args) => void | 向攔截器添加循環(huán)將觸發(fā)循環(huán)鉤子的每個循環(huán)。 |
register | (tap: Tap) => Tap 或 undefined | 將注冊添加到攔截器將觸發(fā)每個添加的Tap,并允許對其進行修改。 |
context
插件和攔截器可以選擇訪問可選的上下文對象,該對象可用于向后續(xù)插件和攔截器傳遞任意值。
我們通過下面的小案例,來幫助我們理解。示例代碼如下:
const { SyncHook } = require("tapable"); // 實例化 鉤子函數(shù) 定義形參 const syncHook = new SyncHook(["name"]); syncHook.intercept({ context: true, register(context, name) { console.log("every time tap", context, name); }, call(context, name) { console.log("before call", context, name); }, loop(context, name) { console.log("before loop", context, name); }, tap(context, name) { console.log("before tap", context, name); }, }); //通過tap函數(shù)注冊事件 syncHook.tap("同步鉤子1", (name) => { console.log("同步鉤子1", name); }); //通過tap函數(shù)注冊事件 syncHook.tap("同步鉤子2", (name) => { console.log("同步鉤子2", name); }); //同步鉤子 通過call 發(fā)布事件 syncHook.call("古茗前端"); syncHook.call("古茗前端 call2");
控制臺輸入結(jié)果如圖所示:
由上面的案例結(jié)果,我們可以知道。intercept
中的 register
會在每一次的 tap
觸發(fā)。 有幾個 tap
就會觸發(fā)幾次 register
。然后依次執(zhí)行鉤子里面的 call
、tap
.
intercept
特性在 webpack 內(nèi)主要被用作進度提示,如 webpack/lib/ProgressPlugin
插件中,分別對 compiler.hooks.emit
、compiler.hooks.afterEmit
鉤子應用了記錄進度的中間件函數(shù)。
HookMap
HookMap
HookMap是具有Hooks的Map的輔助類.提供了一種集合操作能力,能夠降低創(chuàng)建與使用的復雜度,用法比較簡單:
const { SyncHook, HookMap } = require("tapable"); const syncMap = new HookMap(() => new SyncHook()); // 通過 for 函數(shù)過濾集合中的特定鉤子 syncMap.for("some-key").tap("MyPlugin", (arg) => { /* ... */ }); syncMap.for("some-key").tapAsync("MyPlugin", (arg, callback) => { /* ... */ }); syncMap.for("some-key").tapPromise("MyPlugin", (arg) => { /* ... */ }); // 觸發(fā) guming-test 類型的鉤子 syncMap.get("guming-test").call();
Tapable 底層原理
我們先將 Tapable
工程源碼克隆到本地, 執(zhí)行如下指令:
$ git clone https://github.com/webpack/tapable.git
Tapable
源碼的 lib
目錄結(jié)構(gòu), 如下所示:
lib ├─ AsyncParallelBailHook.js ├─ AsyncParallelHook.js ├─ AsyncSeriesBailHook.js ├─ AsyncSeriesHook.js ├─ AsyncSeriesLoopHook.js ├─ AsyncSeriesWaterfallHook.js ├─ Hook.js ├─ HookCodeFactory.js ├─ HookMap.js ├─ MultiHook.js ├─ SyncBailHook.js ├─ SyncHook.js ├─ SyncLoopHook.js ├─ SyncWaterfallHook.js ├─ index.js └─ util-browser.js
除了上面我們所提及的基本 hooks
函數(shù)、HookMap
高級特性,還會有一些 HookCodeFactory
、Hook
這些文件。我們簡單過一下 hooks
函數(shù)內(nèi)的內(nèi)容, 會發(fā)現(xiàn)所有的 hooks
函數(shù)都會引用 HookCodeFactory
和 Hook
這兩個文件所導出的對象實例。
我們以 syncHook
鉤子函數(shù)為例, 如下圖所示:
我們大致能夠知道 一個 hooks
函數(shù) 會由一個 CodeFactory
代碼工廠 以及 Hook
實例組成。 Hook
實例會針對不同場景的 hooks
函數(shù), 更改其對應的 注冊鉤子(tapAsync
,tap
, tapPromise
),事件觸發(fā)鉤子( call
, callAsync
), 編譯函數(shù)(complier)。 Complier
函數(shù)會由我們 HookCodeFactory
實現(xiàn)。
接下來,我們將通過分析 HookCodeFactory
及 Hook
的內(nèi)部實現(xiàn)來了解 Tapable
的內(nèi)部實現(xiàn)機制。
Hook 實例
Hook
實例會生成我們 hooks
鉤子函數(shù)通用的 事件注冊
,事件觸發(fā)
。核心邏輯,我們大致可以分為以下三個部分:
- 實例初始化構(gòu)造函數(shù)
- 事件注冊 的實現(xiàn)
- 事件觸發(fā)的實現(xiàn)
構(gòu)造函數(shù)
構(gòu)造函數(shù)會對實例屬性初始化賦值。代碼如下圖所示:
注冊事件
注冊事件主要分為兩塊,一塊是 適配器注冊調(diào)用
, 第二塊是 觸發(fā)事件注冊
。核心邏輯在 _tap
函數(shù)內(nèi)部實現(xiàn),代碼如下圖所示:
適配器調(diào)用
在這里會對攜帶 register
函數(shù)的適配器進行調(diào)用,更改 options
配置,返回新的 options
配置。代碼如下圖所示:
觸發(fā)事件注冊
Hook
實例的 taps
會存儲我們的注冊事件, 同時會根據(jù),注冊事件配置的執(zhí)行順序去存儲對應的注冊事件。
觸發(fā)事件
觸發(fā)事件會通過調(diào)用內(nèi)部 的 _createCall
函數(shù),函數(shù)內(nèi)部會調(diào)用實例的 compile
函數(shù)。我們會發(fā)現(xiàn):Hook
實例內(nèi)部不會去實現(xiàn) complier
的邏輯, 不同鉤子的 complier
函數(shù)會通過通過對應的 繼承 HookCodeFactory
的實例去實現(xiàn)。代碼如下圖所示:
接下來,我們繼續(xù) 探究 HookCodeFactory
實例,了解 Tapable
事件觸發(fā)的邏輯。
HookCodeFactory 實例
HookCodeFactory
實例會根據(jù)我們傳入的事件觸發(fā)類型 (sync, async, promise
)以及我們的觸發(fā)機制類型 (常規(guī)
瀑布模式
保險模式
循環(huán)模式
), 生成事件觸發(fā)函數(shù)的函數(shù)頭, 函數(shù)體。通過 new Function
構(gòu)造出事件觸發(fā)函數(shù)。
Tapable
事件觸發(fā)的執(zhí)行,是動態(tài)生成執(zhí)行代碼, 包含我們的參數(shù),函數(shù)頭,函數(shù)體,然后通過new Function
來執(zhí)行。相較于我們通常的遍歷/遞歸調(diào)用事件,這無疑讓webpack
的整個事件機制的執(zhí)行有了一個更高的性能優(yōu)勢。
由上面我們可知, Hook
實例 的 complier
函數(shù)是 HookCodeFactory
實例 create
函數(shù) 的返回。
接下來,我們就從 create
函數(shù) 一步步揭秘 Tapable
的動態(tài)生成執(zhí)行函數(shù)的核心實現(xiàn)。
create
create
函數(shù)通過對應的 函數(shù)參數(shù)
, 函數(shù) header
, 函數(shù) content
方法構(gòu)造出我們事件觸發(fā)的函數(shù)的內(nèi)容, 通過 new Function
創(chuàng)建出我們的觸發(fā)函數(shù)。
create 函數(shù)會根據(jù)事件的觸發(fā)類型 ( sync、async、promise),進行不同的邏輯處理。代碼如下圖所示:
每一種觸發(fā)機制,都會由 this.args
, this.header
, this.contentWithInterceptors
三個函數(shù)去實現(xiàn) 動態(tài)函數(shù)的 code
。代碼如下圖所示:
接下來我們看一看 this.contentWithInterceptors
函數(shù)如何生成我們事件觸發(fā)函數(shù)的函數(shù)體。
contentWithInterceptors
contentWithInterceptors
函數(shù)里包含兩個模塊, 一個是適配器 (interceptor
), 一個 content
生成函數(shù)。 同時,HookCodeFactory
實例本身不會去實現(xiàn) content
函數(shù)的邏輯,會由繼承的實例去實現(xiàn)。整體結(jié)構(gòu)代碼如下圖所示:
每個 hooks
鉤子函數(shù)的 CodeFactory
實例會去實現(xiàn) content
函數(shù)。 content
函數(shù)會調(diào)用 HookCodeFactory
實現(xiàn)的不同運行機制的方法( callTap
、callTapsSeries
、callTapsLooping
、callTapsParallel
), 構(gòu)造出最終的函數(shù)體。實現(xiàn)代碼如下圖所示:
接下來,就是不同運行機制,根據(jù)不同的調(diào)用方式 ( sync
, async
, promise
) 生成對應的執(zhí)行代碼。
動態(tài)函數(shù)
我們通過下面一個簡單案例,看 New Function
輸入的內(nèi)容是什么?
實例代碼如下:
const { SyncHook, AsyncSeriesHook } = require("tapable"); // 實例化 鉤子函數(shù) 定義形參 const syncHook = new SyncHook(["name"]); const asyncSeriesHook = new AsyncSeriesHook(["name"]) //通過tap函數(shù)注冊事件 syncHook.tap("同步鉤子1", (name) => { console.log("同步鉤子1", name); }); //通過tap函數(shù)注冊事件 asyncSeriesHook.tapAsync("同步鉤子1", (name) => { console.log("同步鉤子1", name); }); //同步鉤子 通過call 發(fā)布事件 syncHook.call("古茗前端sync"); asyncSeriesHook.callAsync("古茗前端async"); asyncSeriesHook.promise("古茗前端promise")
在 HookCodeFactory
的 create
打印 fn
, 實例代碼如圖所示:
sync
同步調(diào)用的輸出結(jié)果如下:
function anonymous(name ) { "use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; _fn0(name); }
async
異步調(diào)用的輸出結(jié)果如下:
function anonymous(name, _callback ) { "use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; _fn0(name, (function(_err0) { if(_err0) { _callback(_err0); } else { _callback(); } })); }
promise
調(diào)用的輸出結(jié)果如下
function anonymous(name ) { "use strict"; var _context; var _x = this._x; return new Promise((function(_resolve, _reject) { var _sync = true; function _error(_err) { if(_sync) _resolve(Promise.resolve().then((function() { throw _err; }))); else _reject(_err); }; var _fn0 = _x[0]; _fn0(name, (function(_err0) { if(_err0) { _error(_err0); } else { _resolve(); } })); _sync = false; })); }
三種不同方式調(diào)用,內(nèi)部代碼實現(xiàn)差異還是比較清晰的。async
調(diào)用相較于 sync
多了回調(diào)函數(shù)。 async
和 promise
的區(qū)別再去返回 promise
還是回調(diào)函數(shù)。
最后,我們來用一些結(jié)構(gòu)圖來總結(jié)一下 Tapable
中事件觸發(fā)的邏輯。
HookCodeFactory
會根據(jù)我們觸發(fā)的方式,生成我們對應 new Function
里面的 content
, args
, header
。
content
最終會由 callTapsSeries
、callTapsLooping
、callTapsParallel
生成。每種生成方式都會包含 Done
處理、Error
處理以及 Result
處理。
總結(jié)
其他機制的 Hooks
鉤子實現(xiàn)原理大致是相同的, 這里就不一一贅述了。Tapable
是一個非常優(yōu)秀的庫,靈活擴展性高,許多優(yōu)秀的開源項目的插件化設計都借鑒或采納了 Tapable
的設計思想,是一個非常值得推薦學習的一個開源工具庫。
以上就是一文詳解Webpack中Tapable事件機制的詳細內(nèi)容,更多關于Webpack Tapable事件機制的資料請關注腳本之家其它相關文章!
相關文章
TypeScript中的interface與type實戰(zhàn)
這篇文章主要介紹了TypeScript中的interface與type詳解,它們都是用來定義類型的強大工具,在實際開發(fā)中,你可以根據(jù)具體情況選擇使用 interface 或 type,或者甚至將它們結(jié)合起來使用,需要的朋友可以參考下2023-06-06前端技巧之HTTP中POST提交數(shù)據(jù)四種方式
這篇文章主要為大家介紹了前端技巧之HTTP中POST提交數(shù)據(jù)四種方式詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06