一文詳解Webpack中Tapable事件機制
引言
Webpack 是前端工程化常用的靜態(tài)模塊打包工具。在合適的時機通過 Webpack 提供的 API 改變輸出結果,使 Webpack 可以執(zhí)行更廣泛的任務,擁有更強的構建能力。
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í)行結果,參數(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í)行結果如下所示:

異步鉤子
異步鉤子分為: 串行執(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";
});
控制臺輸出結果如下圖所示:

串行鉤子1沒有調(diào)用callback, 所以串行鉤子2沒有觸發(fā)。添加callback后,控制臺輸出結果:

熔斷類
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);
});
控制臺輸出結果如下圖所示:

循環(huán)類
SyncLoopHook 是一個同步、循環(huán)類型的 Hook。循環(huán)類型的含義是不停的循環(huán)執(zhí)行事件函數(shù),直到所有函數(shù)結果 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();
控制臺輸出結果:

瀑布類
AsyncSeriesWaterfallHook 是一個異步串行、瀑布類型的 Hook。如果前一個事件函數(shù)的結果 result !== undefined,則 result 會作為后一個事件函數(shù)的第一個參數(shù)(也就是上一個函數(shù)的執(zhí)行結果會成為下一個函數(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);
});
控制臺輸出結果:

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");
控制臺輸入結果如圖所示:

由上面的案例結果,我們可以知道。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 目錄結構, 如下所示:
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ā)。核心邏輯,我們大致可以分為以下三個部分:
- 實例初始化構造函數(shù)
- 事件注冊 的實現(xiàn)
- 事件觸發(fā)的實現(xiàn)
構造函數(shù)
構造函數(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 構造出事件觸發(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方法構造出我們事件觸發(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)。整體結構代碼如下圖所示:

每個 hooks 鉤子函數(shù)的 CodeFactory 實例會去實現(xiàn) content 函數(shù)。 content 函數(shù)會調(diào)用 HookCodeFactory 實現(xiàn)的不同運行機制的方法( callTap、callTapsSeries、callTapsLooping、callTapsParallel), 構造出最終的函數(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)用的輸出結果如下:
function anonymous(name
) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(name);
}
async 異步調(diào)用的輸出結果如下:
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)用的輸出結果如下
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ù)。
最后,我們來用一些結構圖來總結一下 Tapable 中事件觸發(fā)的邏輯。
HookCodeFactory 會根據(jù)我們觸發(fā)的方式,生成我們對應 new Function 里面的 content, args, header。

content 最終會由 callTapsSeries、callTapsLooping、callTapsParallel 生成。每種生成方式都會包含 Done 處理、Error 處理以及 Result 處理。

總結
其他機制的 Hooks 鉤子實現(xiàn)原理大致是相同的, 這里就不一一贅述了。Tapable 是一個非常優(yōu)秀的庫,靈活擴展性高,許多優(yōu)秀的開源項目的插件化設計都借鑒或采納了 Tapable 的設計思想,是一個非常值得推薦學習的一個開源工具庫。
以上就是一文詳解Webpack中Tapable事件機制的詳細內(nèi)容,更多關于Webpack Tapable事件機制的資料請關注腳本之家其它相關文章!
相關文章
一文帶你徹底搞定JavaScript中字符串轉(zhuǎn)駝峰問題
前端小伙伴們,有沒有被命名規(guī)范逼瘋過,今天咱們就用5步徹底搞定字符串轉(zhuǎn)駝峰的問題,以后再也不用手動改命名啦,快跟隨小編一起學習一下吧2025-05-05
javascript實現(xiàn)前端input密碼輸入強度驗證
這篇文章主要為大家詳細介紹了javascript實現(xiàn)前端input密碼輸入強度驗證,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-06-06
bootstrap模態(tài)框?qū)崿F(xiàn)拖拽效果
這篇文章主要為大家詳細介紹了bootstrap模態(tài)框?qū)崿F(xiàn)拖拽效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12

