webpack打包是一種事件流的機(jī)制,它的原理是將各個插件串聯(lián)起來,那么實現(xiàn)這一切的核心就是我們要講解的tapable. 并且在webpack中負(fù)責(zé)編譯的Compiler和負(fù)責(zé)創(chuàng)建bundles的Compilation都是tapable構(gòu)造函數(shù)的實列。
WebPack的loader(加載器)和plugin(插件)是由Webpack開發(fā)者和社區(qū)開發(fā)者共同貢獻(xiàn)的。如果我們想寫加載器和插件的話,我們需要看懂Webpack的基本原理,也就是說要看懂Webpack源碼,我們今天要講解的 tapable 則是webpack依賴的核心庫。因此在看懂webpack之前,我們先要把 tapable 這個代碼看懂。
我這邊講解的是基于 "webpack": "^4.16.1",這個版本的包的。安裝該webpack之后,該webpack會自帶 tapable 包。在tapable包下它是由如下js文件組成的。
|---- tapable
| |--- AsyncParallelBailHook.js
| |--- AsyncParallelHook.js
| |--- AsyncSeriesBailHook.js
| |--- AsyncSeriesHook.js
| |--- AsyncSeriesLoopHook.js
| |--- AsyncSeriesWaterfallHook.js
| |--- Hook.js
| |--- HookCodeFactory.js
| |--- HookMap.js
| |--- index.js
| |--- MultiHook.js
| |--- simpleAsyncCases.js
| |--- SyncBailHook.js
| |--- SyncHook.js
| |--- SyncLoopHook.js
| |--- SyncWaterfallHook.js
| |--- Tapable.js
如下圖所示:

Tapable的本質(zhì)是能控制一系列注冊事件之間的執(zhí)行流的機(jī)制。如上圖我們可以看到,都是以Sync, Async 及 Hook結(jié)尾的方法,他們?yōu)槲覀兲峁┝瞬煌氖录鲌?zhí)行機(jī)制,我們可以把它叫做 "鉤子"。那么這些鉤子可以分為2個類別,即 "同步" 和 "異步", 異步又分為兩個類別,"并行" 還是 "串行",同步的鉤子它只有 "串行"。
如下圖所示:

下面我會把所有的測試demo放在我們的項目結(jié)構(gòu)下的 public/js/main.js 文件代碼內(nèi),我們可以執(zhí)行即可看到效果。下面是我們項目中的目錄基本結(jié)構(gòu)(很簡單,無非就是一個很簡單的運行本地demo的框架):
可以把該框架下載到本地來,下面的demo可以使用該框架來測試代碼了。
|--- tapable項目
| |--- node_modules
| |--- public
| | |--- js
| | | |--- main.js
| |--- package.json
| |--- webpack.config.js
一:理解Sync類型的鉤子
1. SyncHook.js
SyncHook.js 是處理串行同步執(zhí)行的文件,在觸發(fā)事件之后,會按照事件注冊的先后順序執(zhí)行所有的事件處理函數(shù)。
如下代碼所示:
const { SyncHook } = require('tapable');
// 創(chuàng)建實列
const syncHook = new SyncHook(["name", "age"]);
// 注冊事件
syncHook.tap("1", (name, age) => {
console.log("1", name, age);
});
syncHook.tap("2", (name, age) => {
console.log("2", name, age);
});
syncHook.tap("3", (name, age) => {
console.log("3", name, age);
});
// 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行
syncHook.call("kongzhiEvent-1", 18);
執(zhí)行的結(jié)果如下所示:

如上demo實列可以看到,在我們的tapable中,SyncHook是tapable中的一個類,首先我們需要創(chuàng)建一個實列,注冊事件之前需要創(chuàng)建實列,創(chuàng)建實列時需要傳入一個數(shù)組,該數(shù)組的存儲的事件是我們在注冊事件時,需要傳入的參數(shù)。實列中的tap方法用于注冊事件,該方法支持傳入2個參數(shù),第一個參數(shù)是 '事件名稱', 第二個參數(shù)為事件處理函數(shù),函數(shù)參數(shù)為執(zhí)行call(觸發(fā)事件)時傳入的參數(shù)的形參。
2. SyncBailHook.js
SyncBailHook.js同樣為串行同步執(zhí)行,如果事件處理函數(shù)執(zhí)行時有一個返回值不為空。則跳過剩下未執(zhí)行的事件處理函數(shù)。
如下代碼所示:
const { SyncBailHook } = require('tapable');
// 創(chuàng)建實列
const syncBailHook = new SyncBailHook(["name", "age"]);
// 注冊事件
syncBailHook.tap("1", (name, age) => {
console.log("1", name, age);
});
syncBailHook.tap("2", (name, age) => {
console.log("2", name, age);
return '2';
});
syncBailHook.tap("3", (name, age) => {
console.log("3", name, age);
});
// 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行
syncBailHook.call("kongzhiEvent-1", 18);
如下圖所示:

如上代碼我們可以看到,第一個注冊事件,直接執(zhí)行打印 console.log(); 打印信息出來,它會繼續(xù)執(zhí)行第二個事件,第二個注冊事件有 return '2'; 有返回值,且返回值不為undefined,因此它會跳過后面的注冊事件。因此如上就打印2條信息了。也就是說 syncBailHook 作用也是同步執(zhí)行的,只是說如果我們的注冊事件的回調(diào)函數(shù)有返回值,且返回值不為undefined的話,那么它就會跳過后面的注冊事件。即立刻停止執(zhí)行后面的監(jiān)聽函數(shù)。
3. SyncWaterfallHook.js
SyncWaterfallHook 為串行同步執(zhí)行,上一個事件處理函數(shù)的返回值作為參數(shù)傳遞給下一個事件處理函數(shù),依次類推。
如下測試代碼:
const { SyncWaterfallHook } = require('tapable');
// 創(chuàng)建實列
const syncWaterfallHook = new SyncWaterfallHook(["name", "age"]);
// 注冊事件
syncWaterfallHook.tap("1", (name, age) => {
console.log("第一個函數(shù)事件名稱", name, age);
return '1';
});
syncWaterfallHook.tap("2", (data) => {
console.log("第二個函數(shù)事件名稱", data);
return '2';
});
syncWaterfallHook.tap("3", (data) => {
console.log("第三個函數(shù)事件名稱", data);
return '3';
});
// 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行
const res = syncWaterfallHook.call("kongzhiEvent-1", 18);
console.log(res);
打印信息如下所示:

4. SyncLoopHook.js
SyncLoopHook 為串行同步執(zhí)行,事件處理函數(shù)返回true表示繼續(xù)循環(huán),如果返回undefined的話,表示結(jié)束循環(huán)。
如下代碼演示:
const { SyncLoopHook } = require('tapable');
// 創(chuàng)建實列
const syncLoopHook = new SyncLoopHook(["name", "age"]);
// 定義輔助變量
let total1 = 0;
let total2 = 0;
// 注冊事件
syncLoopHook.tap("1", (name, age) => {
console.log("1", name, age, total1);
return total1++ < 2 ? true : undefined;
});
syncLoopHook.tap("2", (name, age) => {
console.log("2", name, age, total2);
return total2++ < 2 ? true : undefined;
});
syncLoopHook.tap("3", (name, age) => {
console.log("3", name, age);
});
// 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行
syncLoopHook.call("kongzhiEvent-1", 18);
執(zhí)行的結(jié)果如下所示:

執(zhí)行結(jié)果如上所示,我們來理解下 SyncLoopHook 執(zhí)行順序,首先我們知道,SyncLoopHook 的基本原理是:事件處理函數(shù)返回true表示繼續(xù)循環(huán),如果返回undefined的話,表示結(jié)束循環(huán)。
首先我們出發(fā)第一個注冊事件函數(shù),total1 依次循環(huán),所以會打印 0, 1, 2 的值,因此打印的值如下所示:
1 kongzhiEvent-1 18 0
1 kongzhiEvent-1 18 1
1 kongzhiEvent-1 18 2
當(dāng) total1 = 2 的時候,再去判斷 2 < 2 呢?所以最后值返回 undefined, 因此就執(zhí)行第二個回調(diào)函數(shù),但是此時由于 total1++; 因此 total1 = 3了。所以執(zhí)行完成第二個 函數(shù)的時候,會打印信息如下:
2 kongzhiEvent-1 18 0
但是由于等于true,所以會繼續(xù)循環(huán)函數(shù),因此又會從第一個函數(shù)內(nèi)部訓(xùn)話,因此第一個函數(shù)就會打印如下信息:
1 kongzhiEvent-1 18 3
但是由于 total1 = 3了,因此又返回undefined了,因此又會執(zhí)行 第二個函數(shù),這個時候 total2 = 1了,因此會打印:
2 kongzhiEvent-1 18 1
然后返回true,繼續(xù)從第一個函數(shù)循環(huán)執(zhí)行,此時的 total1 = 4; 因為在上次 total1=3 雖然條件不滿足,但是還是會自增1的,因此會繼續(xù)循環(huán)打印如下信息:
1 kongzhiEvent-1 18 4
此時 又不滿足,因此會執(zhí)行第二個函數(shù),此時的 total2=2了,因為在上一次執(zhí)行完成后,total2會自增1. 因此先打印如下信息:
2 kongzhiEvent-1 18 2
由于此時 total2 = 2; 因此最后返回undefined,因此會執(zhí)行第三個函數(shù),但是此時的 total2 = 3了,因為執(zhí)行了 total2++; 所以最后一個函數(shù)會打印如下信息:
3 kongzhiEvent-1 18
如上就是 SyncLoopHook.js 函數(shù)的作用。
二:理解Async類型的鉤子
Async類型可以使用tap, tapSync 和 tapPromise 注冊不同類型的插件鉤子,我們分別可以通過 call, callAsync, promise 方法調(diào)用。
1. AsyncParallelHook
AsyncParallelHook 為異步并行執(zhí)行,如果是通過 tapAsync 注冊的事件,那么我們需要通過callAsync觸發(fā),如果我們通過tapPromise注冊的事件,那么我們需要promise觸發(fā)。
1)tapAsync/callAsync
如下代碼所示:
const { AsyncParallelHook } = require('tapable');
// 創(chuàng)建實列
const asyncParallelHook = new AsyncParallelHook(["name", "age"]);
// 注冊事件
asyncParallelHook.tapAsync("1", (name, age, done) => {
setTimeout(() => {
console.log("1", name, age, new Date());
done();
}, 1000);
});
asyncParallelHook.tapAsync("2", (name, age, done) => {
setTimeout(() => {
console.log("2", name, age, new Date());
done();
}, 2000);
});
asyncParallelHook.tapAsync("3", (name, age, done) => {
setTimeout(() => {
console.log("3", name, age, new Date());
done();
}, 3000);
});
// 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行
asyncParallelHook.callAsync("kongzhiEvent-1", 18, () => {
console.log('函數(shù)執(zhí)行完畢');
});
執(zhí)行結(jié)果如下所示:

如上我們可以看到,該三個函數(shù),第一個函數(shù)是 2019 17:10:55,第二個函數(shù)是 2019 17:10:56,第三個函數(shù)是 2019 17:10:57 執(zhí)行完成了,上面三個函數(shù)定時器操作最長的時間也是3秒,我們把這三個函數(shù)執(zhí)行完成總共也是使用了3秒的時間,說明了該三個事件處理函數(shù)是異步執(zhí)行的了。不需要等待上一個函數(shù)結(jié)束后再執(zhí)行下一個函數(shù)。
tapAsync注冊的事件函數(shù)最后一個參數(shù)為回調(diào)函數(shù)done,每個事件處理函數(shù)在異步代碼執(zhí)行完成后都會調(diào)用該done函數(shù)。因此就能保證我們的 callAsync會在所有異步函數(shù)執(zhí)行完畢后就執(zhí)行該回調(diào)函數(shù)。
2)tapPromise/promise
使用tapPromise注冊的事件,必須返回一個Promise實列,promise方法也會返回一個Promise實列。
如下代碼演示:
const { AsyncParallelHook } = require('tapable');
// 創(chuàng)建實列
const asyncParallelHook = new AsyncParallelHook(["name", "age"]);
// 注冊事件
asyncParallelHook.tapPromise("1", (name, age) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("1", name, age, new Date());
}, 1000);
});
});
asyncParallelHook.tapPromise("2", (name, age) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("2", name, age, new Date());
}, 2000);
});
});
asyncParallelHook.tapPromise("3", (name, age) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("3", name, age, new Date());
}, 3000);
});
});
// 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行
asyncParallelHook.promise("kongzhiEvent-1", 18);
效果如下所示:

如上代碼所示,每個tabPromise注冊事件的處理函數(shù)都會返回一個Promise實列,新版的代碼可能改掉了,我們不能再promise函數(shù)內(nèi)部使用 resolve('1') 這樣的,在外部不能使用 asyncParallelHook.promise("kongzhiEvent-1", 18).then() 這樣的,不支持then,剛看了源碼,也沒有返回一個新的promise對象。
如上也可以看到,我們的第一個函數(shù)需要1秒后執(zhí)行,第二個函數(shù)需要2秒后執(zhí)行,第三個函數(shù)需要三秒后執(zhí)行,但是我們打印的信息可以看到,總共花費了3秒時間,也就是說我們上面的三個函數(shù)也是并行執(zhí)行的。并不是需要等前一個函數(shù)執(zhí)行完畢后再執(zhí)行后面的函數(shù)。
2. AsyncSeriesHook
AsyncSeriesHook 為異步串行執(zhí)行的。和我們上面的 AsyncParallelHook一樣,通過使用 tapAsync注冊事件,通過callAsync觸發(fā)事件,也可以通過 tapPromise注冊事件,使用promise來觸發(fā)。
1)tapAsync/callAsync
和我們上面的 AsyncParallelHook一樣, AsyncParallelHook 的 callAysnc方法也是通過傳入回調(diào)函數(shù)的方式,在所有事件函數(shù)處理完成后,我們需要執(zhí)行 callAsync的回調(diào)函數(shù)。
如下代碼演示:
const { AsyncSeriesHook } = require('tapable');
// 創(chuàng)建實列
const asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);
// 注冊事件
asyncSeriesHook.tapAsync("1", (name, age, done) => {
setTimeout(() => {
console.log("1", name, age, new Date());
done();
}, 1000);
});
asyncSeriesHook.tapAsync("2", (name, age, done) => {
setTimeout(() => {
console.log("2", name, age, new Date());
done();
}, 2000);
});
asyncSeriesHook.tapAsync("3", (name, age, done) => {
setTimeout(() => {
console.log("3", name, age, new Date());
done();
}, 3000);
});
// 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行
asyncSeriesHook.callAsync("kongzhiEvent-1", 18, () => {
console.log('執(zhí)行完成');
});
執(zhí)行結(jié)果如下所示:

從上面打印信息我們可以看到,我們第一次打印 1 kongzhiEvent-1 18 Mon Aug 05 2019 17:58:09 GMT+0800 (中國標(biāo)準(zhǔn)時間) 這個信息,然后當(dāng)我們執(zhí)行第二個函數(shù)的時候,是2000毫秒后執(zhí)行,因此打印第二條信息如下所示:
2 kongzhiEvent-1 18 Mon Aug 05 2019 17:58:11 GMT+0800 (中國標(biāo)準(zhǔn)時間)
接著我們執(zhí)行第三個函數(shù),隔了3000毫秒后執(zhí)行,因此可以看到打印信息如下:
3 kongzhiEvent-1 18 Mon Aug 05 2019 17:58:14 GMT+0800 (中國標(biāo)準(zhǔn)時間)
因此我們可以看到該方法是串行執(zhí)行的。
2)tapPromise/promise
和上面的 AsyncParallelHook 一樣,使用tapPromise來注冊事件函數(shù),然后需要返回一個Promise實列,然后我們使用 promise 來觸發(fā)該事件。
如下代碼所示:
const { AsyncSeriesHook } = require('tapable');
// 創(chuàng)建實列
const asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);
// 注冊事件
asyncSeriesHook.tapPromise("1", (name, age) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("1", name, age, new Date());
resolve();
}, 1000);
})
});
asyncSeriesHook.tapPromise("2", (name, age) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("2", name, age, new Date());
resolve();
}, 2000);
});
});
asyncSeriesHook.tapPromise("3", (name, age) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("3", name, age, new Date());
resolve();
}, 3000);
});
});
// 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行
asyncSeriesHook.promise("kongzhiEvent-1", 18);
如上代碼執(zhí)行效果如下所示:

如上代碼,我們對比 AsyncParallelHook 代碼可以看到,唯一不同的是 該asyncSeriesHook的Promsie內(nèi)部需要調(diào)用 resolve() 函數(shù)才會執(zhí)行到下一個函數(shù),否則的話,只會執(zhí)行第一個函數(shù),但是 AsyncParallelHook 不調(diào)用 resolve()方法會依次執(zhí)行下面的函數(shù)。
三:tapable源碼分析
先以SyncHook.js 源碼分析:
源碼如下:
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
const factory = new SyncHookCodeFactory();
class SyncHook extends Hook {
tapAsync() {
throw new Error("tapAsync is not supported on a SyncHook");
}
tapPromise() {
throw new Error("tapPromise is not supported on a SyncHook");
}
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
module.exports = SyncHook;
如上代碼我們可以看到 SyncHook 類它繼承了 Hook 類,然后 定義了 SyncHookCodeFactory 類 繼承了 HookCodeFactory 類,我們先來看看 Hook.js 相關(guān)的代碼如下:
class Hook {
constructor(args) {
// args 參數(shù)必須是一個數(shù)組,比如我們上面的demo 傳遞值為 ["name", "age"]
if(!Array.isArray(args)) args = [];
// 把數(shù)組args賦值給 _args的內(nèi)部屬性
this._args = args;
// 保存所有的tap事件
this.taps = [];
// 攔截器數(shù)組
this.interceptors = [];
// 調(diào)用 內(nèi)部方法 _createCompileDelegate 然后把返回值賦值給內(nèi)部屬性 _call, 并且暴露給外部屬性 call
this.call = this._call = this._createCompileDelegate("call", "sync");
/* 調(diào)用 內(nèi)部方法 _createCompileDelegate ,然后把返回值賦值給內(nèi)部屬性 _promise,并且暴露外部屬性 promise
*/
this.promise = this._promise = this._createCompileDelegate("promise", "promise");
/*
調(diào)用 內(nèi)部方法 _createCompileDelegate,然后把返回值賦值給內(nèi)部屬性 _callAsync, 并且暴露外部屬性
callAsync
*/
this.callAsync = this._callAsync = this._createCompileDelegate("callAsync", "async");
// 用于調(diào)用函數(shù)的時候,保存鉤子數(shù)組的變量
this._x = undefined;
}
}
如上 類 Hook 是代碼的初始化工作;對于上面的 _createCompileDelegate 這個方法,我們先不用管,該方法在我們的這個SyncHook 類暫時還用不上,因為這個是處理異步的操作,下面我們會講解到的。下面我們需要看 注冊事件 tap 方法.
代碼如下:
tap(options, fn) {
if(typeof options === "string")
options = { name: options };
if(typeof options !== "object" || options === null)
throw new Error("Invalid arguments to tap(options: Object, fn: function)");
options = Object.assign({ type: "sync", fn: fn }, options);
if(typeof options.name !== "string" || options.name === "")
throw new Error("Missing name for tap");
// 注冊攔截器
options = this._runRegisterInterceptors(options);
// 插入鉤子
this._insert(options);
}
如上該方法接收2個參數(shù),第一個參數(shù)必須是一個字符串,如果它是字符串的話,那么 options = {name: options}, options 就返回了一個帶name屬性的對象了。然后使用 Object.assign()方法對對象合并,如下代碼:options = Object.assign({ type: "sync", fn: fn }, options); 因此最后options就返回如下了:
options = { type: "sync", fn: fn, name: options };
然后就是調(diào)用如下方法,來注冊一個攔截器;如下代碼:
options = this._runRegisterInterceptors(options);
現(xiàn)在我們來看下 _runRegisterInterceptors 代碼如下所示:
_runRegisterInterceptors(options) {
for(const interceptor of this.interceptors) {
if(interceptor.register) {
const newOptions = interceptor.register(options);
if(newOptions !== undefined)
options = newOptions;
}
}
return options;
}
如上_runRegisterInterceptors() 方法是注冊攔截器的方法,該方法有一個options參數(shù),該options的值是:
options = { type: "sync", fn: fn, name: '這是一個字符串' };
在該函數(shù)內(nèi)部我們遍歷攔截器this.interceptors,然后攔截器 this.interceptors會有一個屬性register,如果有該屬性的話,就調(diào)用該屬性來注冊一個新的 newOptions 對象,如果 newOptions 對象 不等于 undefined的話,就把options = newOptions; 賦值給 options的 最后返回 options, 當(dāng)然如果沒有該攔截器的話,就直接返回該 options對象。
如上代碼我們知道,我們在調(diào)用 tap()方法來注冊事件之前,我們就需要使用注冊攔截器,來添加攔截器的,因為在調(diào)用_runRegisterInterceptors 方法時,它內(nèi)部代碼會遍歷該攔截器,因此我們就可以判定在我們使用 tap 事件之前,我們就需要使用 添加攔截器. 下面我們來看看我們添加攔截器的代碼如下:
intercept(interceptor) {
// 重置所有的調(diào)用方法
this._resetCompilation();
// 保存攔截器到全局屬性 interceptors內(nèi)部,我們使用 Object.assign方法復(fù)制了一份
this.interceptors.push(Object.assign({}, interceptor));
/*
如果該攔截器有register屬性的話,我們就遍歷所有的taps, 把他們作為參數(shù)調(diào)用攔截器的register,并且把返回的tap對象
(該tap對象指tap函數(shù)里面把fn和name這些信息組合起來的新對象)。然后賦值給 當(dāng)前的某一項tap
*/
if(interceptor.register) {
for(let i = 0; i < this.taps.length; i++)
this.taps[i] = interceptor.register(this.taps[i]);
}
}
下面我們看下如下demo來繼續(xù)理解下 intercept 方法的含義:如下demo所示:
const { SyncHook } = require('tapable');
const h1 = new SyncHook(['xxx']);
h1.tap('A', function(args) {
console.log('A', args);
return 'b';
});
h1.tap('B', function() {
console.log('b');
});
h1.tap('C', function() {
console.log('c');
});
h1.tap('D', function() {
console.log('d');
});
h1.intercept({
call: (...args) => {
console.log(...args, '11111111');
},
register: (tap) => {
console.log(tap, '222222');
return tap;
},
loop: (...args) => {
console.log(...args, '33333');
},
tap: (tap) => {
console.log(tap, '444444');
}
});
運行效果如下所示:

如上demo代碼我們可以看到,我們在調(diào)用 tap 來注冊我們的事件的時候,我們先會執(zhí)行我們的攔截器,也就是調(diào)用我們的SyncHook類的實列對象 h1, 會調(diào)用 h1.intercept 方法的 register 函數(shù),所以我們注冊了多少次,就使用攔截器攔截了多少次,并且返回了一個新的對象, 比如返回了 {type: "sync", fn: fn, name: 'A'} 這樣的新對象。
我們可以在返回看下我們的 Hook.js 中的 tap(options, fn) {} 這個方法內(nèi)部,該方法內(nèi)部注冊事件的時候,會先調(diào)用
options = this._runRegisterInterceptors(options);
這個函數(shù)代碼,該函數(shù)代碼的作用是注冊攔截器,然后返回新的對象回來,如下代碼所示:
_runRegisterInterceptors(options) {
for(const interceptor of this.interceptors) {
if(interceptor.register) {
const newOptions = interceptor.register(options);
if(newOptions !== undefined)
options = newOptions;
}
}
return options;
}
如上我們可以看到,它會返回了一個新對象,就是我們上面打印出來的對象。該值會保存到我們的 options 參數(shù)中,接著我們繼續(xù)執(zhí)行 taps函數(shù)中的最后一句代碼:
this._insert(options);
該函數(shù)的代碼,就是把所有的事件對象保存到 this.taps 中。保存完成后,那么 this.taps 就有該值了,然后這個時候我們就會調(diào)用我們上面的 intercept 中的 register 這個函數(shù)。 下面我們繼續(xù)來看下 _insert(options) 中的代碼吧,代碼如下所示:
_insert(item) {
// 重置資源,因為每一個插件都會有一個新的 Compilation
this._resetCompilation();
// 該item.before 是插件的名稱
let before;
// 打印item
console.log(item);
/*
before 可以是單個字符串插件的名稱,也可以是一個字符串?dāng)?shù)組的插件
new Set 是ES6新增的,它的作用是去掉數(shù)組里面重復(fù)的值
*/
if(typeof item.before === "string")
before = new Set([item.before]);
else if(Array.isArray(item.before)) {
before = new Set(item.before);
}
let stage = 0;
if(typeof item.stage === "number")
stage = item.stage;
let i = this.taps.length;
while(i > 0) {
console.log('----', i);
i--;
const x = this.taps[i];
this.taps[i+1] = x;
const xStage = x.stage || 0;
if(before) {
if(before.has(x.name)) {
before.delete(x.name);
continue;
}
if(before.size > 0) {
continue;
}
}
if(xStage > stage) {
continue;
}
i++;
break;
}
// 打印i的值
console.log(i);
this.taps[i] = item;
}
如上代碼使用while循環(huán),遍歷所有的taps的函數(shù),然后會根據(jù)stage和before進(jìn)行重新排序,stage的優(yōu)先級低于before。如下demo,我們也可以如下調(diào)用 tap, 比如tap是一個數(shù)組。如下所示:
const { SyncHook } = require('tapable');
const h1 = new SyncHook(['xxx']);
h1.tap('A', function(args) {
console.log('A', args);
return 'b';
});
h1.tap('B', function() {
console.log('b');
});
h1.tap('C', function() {
console.log('c');
});
h1.tap({
name: 'F',
before: 'D'
}, function() {
});
h1.tap({
name: 'E',
before: 'C'
}, function() {
});
h1.tap('D', function() {
console.log('d');
});
h1.intercept({
call: (...args) => {
console.log(...args, '11111111');
},
register: (tap) => {
console.log(tap, '222222');
return tap;
},
loop: (...args) => {
console.log(...args, '33333');
},
tap: (tap) => {
console.log(tap, '444444');
}
});
打印后的效果如下所示:

如上我們可以看到我們的 _insert(item); 方法中打印的console.log(item);項的值及打印console.log(i)的值及console.log('----', i); 我們可以看下如上的 _insert(item)方法中的算法如下理解:
1. 首先在我們的代碼demo里面使用 tap 注冊一個A事件,h1.tap('A', function(args) {}), 因此最后會把該函數(shù)返回的對象傳遞進(jìn)來,對象為 {type: 'sync', fn: fn, name: 'A'}; 因此打印的 console.log(item); 就是該對象值,初始化第一步i的值為0. 因為 this.taps.length 的長度為0. 所以第一次不會進(jìn)入 while循環(huán)內(nèi)部,執(zhí)行到最后我們就把 console.log(i) 的值打印出來。
2. 當(dāng)我們使用 tap注冊一個B事件的時候, h1.tap('B', function(args) {}); console.log(item); 就會打印出對象的值為:
{type: 'sync', fn:fn, name: 'B'}; 然后判斷是否有before或state這個屬性,如果沒有的話,直接跳過,while (i > 0); 進(jìn)入該while循環(huán)內(nèi)部,打印 console.log('----', i); 因此會打印 '---- 1' 這樣的,i--, 執(zhí)行完后 i 的值會減一. const x = this.taps[i]; 因此 x = this.taps[0] = {type: 'sync', fn: fn, name: 'A'}, this.taps[i+1] = x; 因此 this.taps[1] = {type: 'sync', fn: fn, name: 'A'}; 這樣的。const xStage = x.stage || 0; 因此 xStage = 0; 因為我們沒有stage這個屬性,也沒有before屬性,所以也不會進(jìn)入上面的if語句,最后我們會進(jìn)行如下判斷:
if(xStage > stage) {
continue;
}
也不會進(jìn)入if語句,然后 i++; 因此此時 i = 1 了;因此會打印1. 此時我們的 this.taps = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}];
3. 當(dāng)我們使用tap注冊一個C事件的時候,h1.tap('C', function() {}); 同理,打印出我們的 console.log(item) 的值變?yōu)槿缦拢?br> {type: "sync", fn: ?, name: "C"},一樣也沒有before和state屬性,如果沒有,直接跳過,接著就打印 ---2 然后此時 i=2了,最后我們的
this.taps = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}, {type: "sync", fn: ?, name: "C"}];
4. 當(dāng)我們注冊F事件的時候,如代碼:h1.tap({name: 'F', before: 'D'}, function() {}), 此時 i = this.taps.length = 3; 因此第一次會打印 '---- 3';
const x = this.taps[i]; const x = this.taps[2] = {type: "sync", fn: ?, name: "C"}; this.taps[i+1] = x; 因此 this.taps[3] = {type: "sync", fn: ?, name: "C"}; 然后會判斷是否有before或state這個屬性,我們注冊F事件的時候可以看到,它有before這個屬性了,因此在內(nèi)部i的值會從3依次循環(huán),直接0為止,每次循環(huán)內(nèi)部自己減少1. 因此最后我們的i的值就變?yōu)?了,因此我們的 this.tabs值就變成這樣的了:
this.tabs = [
{type: 'sync', name: 'F', before:'D', fn: fn},
{type: 'sync', fn: fn, name: 'A'},
{type: 'sync', fn: fn, name: 'B'},
{type: "sync", fn: ?, name: "C"}
];
5. 當(dāng)我們注冊事件E的時候,如代碼 h1.tap({name: 'E', before: 'C'}, function(){}); 此時我們的i = 4了,因此會打印 '---- 4',
const x = this.taps[i]; const x = this.taps[3] = {type: "sync", fn: ?, name: "C"}; this.taps[i+1] = this.taps[4] = {type: "sync", fn: ?, name: "C"}; 然后會判斷是否有before或state這個屬性,我們注冊E事件的時候可以看到,它有before這個屬性了,
后面邏輯依次類推....
上面代碼分析的優(yōu)點煩,我們再來整理下思路,理解下 上面的算法:
1. 假如我們注冊了如下代碼:
h1.tap('A', function(args) {});
h1.tap('B', function(args) {});
h1.tap('C', function(args) {});
h1.tap({name: 'F', before: 'D'}, function(args) {});
h1.tap({ name: 'E', before: 'C'}, function() {});
h1.tap('D', function() { console.log('d'); });
如上注冊了這么多函數(shù),也就是說,我們每次注冊一個函數(shù)都會傳遞一個對象進(jìn)來,
比如類似這樣的:
{type: 'sync', fn: fn, name: 'A'},
{type: 'sync', fn: fn, name: 'B'},
{type: 'sync', fn: fn, name: 'C'},
{type: 'sync', fn: fn, name: 'F', before: 'D'},
{type: 'sync', fn: fn, name: 'E', before: 'C'},
{type: 'sync', fn: fn, name: 'D'}.
會依次調(diào)用我們的 _insert(item) 這個函數(shù),item的值就是上面我們的依次循環(huán)的每個對象的值。
2. 第一次傳遞 {type: 'sync', fn: fn, name: 'A'} 這個對象進(jìn)來后,由于第一次我們的 let i = this.taps.length; 的長度為0;因此就不會進(jìn)行 代碼的while內(nèi)部循環(huán),因此我們的 this.taps = [{type: 'sync', fn: fn, name: 'A'}]; 這樣的值。
3. 第二次傳遞 {type: 'sync', fn: fn, name: 'B'} 這個對象進(jìn)來的時候,這次我們的 let i = this.taps.length; 的長度為1了,因此
就會進(jìn)入while循環(huán),const x = this.taps[i]; const x = {type: 'sync', fn: fn, name: 'A'}; this.taps[i+1] = {type: 'sync', fn: fn, name: 'A'}; 因此這個時候 我們的 this.taps = [{type: 'sync', fn: fn, name: 'A'},{type: 'sync', fn: fn, name: 'A'}] 了。由于該對象事件沒有 stage 或 before 這個參數(shù),因此最后執(zhí)行 i++; 因此i的值變?yōu)?,最后一句代碼:this.taps[i] = item; item的值就是我們的 這個對象 {type: 'sync', fn: fn, name: 'B'}; 因此此時的 this.taps 的值,變?yōu)槿缦拢?/p>
this.taps = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}];
4. 第三次傳遞 {type: 'sync', fn: fn, name: 'C'} 這個對象進(jìn)來的時候,和我們的第三步驟一樣,依次類推,因此最后我們的 this.taps 的值變?yōu)槿缦拢?/p>
this.taps = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}, {type: 'sync', fn: fn, name: 'C'}];
5. 第四次傳遞 {type: 'sync', fn: fn, name: 'F', before: 'D'} 這個對象進(jìn)來的時候,此時我們的 let i = this.taps.length = 3 了; 因此就會進(jìn)入while循環(huán),執(zhí)行如下代碼:i--; const x = this.taps[i]; this.taps[i+1] = x; 因此
const x = this.taps[2]
= {type: 'sync', fn: fn, name: 'C'}; this.taps[i+1] = this.taps[3] = {type: 'sync', fn: fn, name: 'C'};
因此此時我們的 this.taps 對象的值變?yōu)槿缦拢?/p>
this.taps = [
{type: 'sync', fn: fn, name: 'A'},
{type: 'sync', fn: fn, name: 'B'},
{type: 'sync', fn: fn, name: 'C'},
{type: 'sync', fn: fn, name: 'C'}
];
最后我們就會執(zhí)行下面的代碼:
if(before) {
if(before.has(x.name)) {
before.delete(x.name);
continue;
}
if(before.size > 0) {
continue;
}
}
因為事件F有before這個參數(shù),因此會進(jìn)入if條件判斷語句了,接著就判斷 before.has(x.name); 判斷該對象是否有 before 該值,比如我們上面的的before為字符串 'D', 判斷我們之前保存的 this.taps 數(shù)組內(nèi)部的每項對象的name屬性是否有 'D' 這個字符串。如果有的話,就直接刪除該對象。 所以一直沒有找到 'D' 字符,因此會一直判斷 if(before.size > 0) { continue; } 進(jìn)行對內(nèi)部i循環(huán),因此i = 3;就循環(huán)了3次,依次是3, 2, 1 這樣的,最后 i-- ;i = 0的時候,就不會進(jìn)入while循環(huán)內(nèi)部了,因此我們在第一個位置會插入 F事件了,比如:
this.taps[0] = {type: 'sync', fn: fn, name: 'F', before: 'D'};
注意:上面再內(nèi)部依次循環(huán) 3, 2, 1 的時候,我們的this.taps的值數(shù)組會發(fā)生改變的,比如等于3的時候,我們的數(shù)組是如下這個樣子,因為執(zhí)行了如下代碼:
i--;
const x = this.taps[i];
this.taps[i+1] = x;
i = 3 時,this.taps的值為 = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}, {type: 'sync', fn: fn, name: 'C'}, {type: 'sync', fn: fn, name: 'C'}];
i = 2 時,this.taps的值為 = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}, {type: 'sync', fn: fn, name: 'B'}, {type: 'sync', fn: fn, name: 'C'}];
i = 1時,this.taps的值為 = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}, {type: 'sync', fn: fn, name: 'C'}];
最后也就是我們上面的 i = 0的時候,因此我們會把 {type: 'sync', fn: fn, name: 'F', before: 'D'}; 對象值插入到我們的 taps數(shù)組的第一個位置上了,因此 this.taps的值最終變?yōu)椋篬{type: 'sync', fn: fn, name: 'F', before: 'D'}, {type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}, {type: 'sync', fn: fn, name: 'C'}];
6. 第五次傳遞 {type: 'sync', fn: fn, name: 'E', before: 'C'} 的時候,也是同樣的道理,此時我們的 let i = this.taps.length = 4 了; 因此就會進(jìn)入while循環(huán),執(zhí)行如下代碼:i--; const x = this.taps[i]; this.taps[i+1] = x; 因此我們的 const x = this.taps[3] = {type: 'sync', fn: fn, name: 'C'}; this.taps[i+1] = this.taps[4] = {type: 'sync', fn: fn, name: 'C'};
在while內(nèi)部循環(huán),同樣的道理也會執(zhí)行如下代碼:
if(before) {
if(before.has(x.name)) {
before.delete(x.name);
continue;
}
if(before.size > 0) {
continue;
}
}
if(xStage > stage) {
continue;
}
i++;
break;
首先是有before這個參數(shù)的,因此會進(jìn)入if語句內(nèi)部,然后判斷該before是否有該 x.name 屬性嗎?before的屬性值為 'C'; 因此判斷該有沒有x.name 呢,我們從上面知道我們的 this.taps 的值為 =
[
{type: 'sync', fn: fn, name: 'F', before: 'D'},
{type: 'sync', fn: fn, name: 'A'},
{type: 'sync', fn: fn, name: 'B'},
{type: 'sync', fn: fn, name: 'C'},
{type: 'sync', fn: fn, name: 'C'}
];
從上面 i-- 可知,我們此時i的值為3,因此我們需要把該值插入到 this.taps[3] = {type: 'sync', fn: fn, name: 'E', before: 'C'} 了; 因此此時 this.taps的值就變?yōu)槿缦铝耍?/p>
this.taps = [
{type: 'sync', fn: fn, name: 'F', before: 'D'},
{type: 'sync', fn: fn, name: 'A'},
{type: 'sync', fn: fn, name: 'B'},
{type: 'sync', fn: fn, name: 'E', before: 'C'},
{type: 'sync', fn: fn, name: 'C'}
];
7. 第六次傳遞的D事件,也是一個意思,這里就不再分析了,因此我們的 this.taps的值最終變?yōu)槿缦拢?/p>
this.taps = [
{type: 'sync', fn: fn, name: 'F', before: 'D'},
{type: 'sync', fn: fn, name: 'A'},
{type: 'sync', fn: fn, name: 'B'},
{type: 'sync', fn: fn, name: 'E', before: 'C'},
{type: 'sync', fn: fn, name: 'C'},
{type: 'sync', fn: fn, name: 'D'}
];
如上就是一個排序算法; 大家可以理解下。至于stage屬性也是一樣的,只是before屬性的優(yōu)先級相對于stage會更高。
理解 _createCompileDelegate() 函數(shù)代碼
我們再回到我們的 Hook.js 中的構(gòu)造函數(shù)內(nèi)部有如下幾句代碼:
class Hook {
constructor(args) {
this.call = this._call = this._createCompileDelegate("call", "sync");
this.promise = this._promise = this._createCompileDelegate("promise", "promise");
this.callAsync = this._callAsync = this._createCompileDelegate("callAsync", "async");
}
}
如上代碼,我們可以看到我們的 this.call, this.promise, this.callAsync 都會調(diào)用 內(nèi)部函數(shù) _createCompileDelegate, 我們來看看該內(nèi)部函數(shù)的代碼如下所示:
_createCompileDelegate(name, type) {
const lazyCompileHook = (...args) => {
this[name] = this._createCall(type);
return this[name](...args);
};
return lazyCompileHook;
}
如上可以看到,_createCompileDelegate 函數(shù)接收2個參數(shù),name 和 type,就是我們上面調(diào)用該函數(shù)的時候傳遞進(jìn)來的。然后在內(nèi)部使用閉包的形式返回了 lazyCompileHook 函數(shù),因此 this.call, this.promise, this.callAsync 都返回了該函數(shù) lazyCompileHook 。
我們再來看下如上demo,加上如下測試代碼如下所示:
const { SyncHook } = require('tapable');
const h1 = new SyncHook(['xxx']);
h1.tap('A', function(args) {
console.log('A', args);
return 'b';
});
h1.tap('B', function() {
console.log('b');
});
h1.tap('C', function() {
console.log('c');
});
h1.tap({
name: 'F',
before: 'D'
}, function() {
});
h1.tap({
name: 'E',
before: 'C'
}, function() {
});
h1.tap('D', function() {
console.log('d');
});
h1.call(7777);
如上我們打印的結(jié)果如下所示:

如上我們調(diào)用call方法后,會因此執(zhí)行 如上面的注冊事件的回調(diào)函數(shù),我們再來看下_createCompileDelegate函數(shù)內(nèi)部代碼
_createCompileDelegate(name, type) {
const lazyCompileHook = (...args) => {
this[name] = this._createCall(type);
return this[name](...args);
};
return lazyCompileHook;
}
該函數(shù)內(nèi)部代碼,返回了lazyCompileHook函數(shù)給我們的call對象,然后當(dāng)我們的 this.call(7777)的時候就會調(diào)用lazyCompileHook函數(shù),傳遞了一個參數(shù),因此 ...args = 7777; 內(nèi)部代碼:
this[name] = this._createCall(type); 也就是說 這邊的this對象指向了 SyncHook 的實列了,也就是我們外面的實列 h1對象了,this['call'] = this._createCall('sync'); 我們下面看下 _createCall 函數(shù)代碼如下:
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
compile(options) {
throw new Error("Abstract: should be overriden");
}
如上代碼,我們是不是萌了?compile方法直接拋出一個對象?當(dāng)然不是,我們在 SyncHook 這個類中(其他的類也是一樣),會對該方法進(jìn)行重寫的,我們可以看下我們的 SyncHook中的類代碼,如下所示:
const HookCodeFactory = require("./HookCodeFactory");
const factory = new SyncHookCodeFactory();
class SyncHook extends Hook {
tapAsync() {
throw new Error("tapAsync is not supported on a SyncHook");
}
tapPromise() {
throw new Error("tapPromise is not supported on a SyncHook");
}
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
如上我們可以看到 我們的 compile 方法會進(jìn)行重寫該方法。如上的compile方法中的options的參數(shù)值就是我們上面?zhèn)鬟f進(jìn)來的,如下所示
options = {
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
}
如上看到,我們引用了 HookCodeFactory 類進(jìn)來,并且使用了 該類的實列 factory 中的 setUp()方法及 create()方法,我們看下該 HookCodeFactory 類代碼如下所示:
class HookCodeFactory {
constructor(config) {
this.config = config;
this.options = undefined;
}
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
create(options) {
}
}
如上 setup 中的參數(shù) instance 就是調(diào)用該實列對象了,options的參數(shù)值就是我們在 SyncHook.js 的參數(shù)如下值:
options = {
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
}
其中 this.taps 值就是我們的上面的那個數(shù)組。 比如 :
this.taps = [
{type: 'sync', fn: fn, name: 'F', before: 'D'},
{type: 'sync', fn: fn, name: 'A'},
{type: 'sync', fn: fn, name: 'B'},
{type: 'sync', fn: fn, name: 'E', before: 'C'},
{type: 'sync', fn: fn, name: 'C'},
{type: 'sync', fn: fn, name: 'D'}
];
這樣的, 然后每個事件對象的實列都綁定到 instance._x = fn. 這里面的fn就是我們this.taps數(shù)組里面遍歷的fn函數(shù)。
每個注冊事件對應(yīng)一個函數(shù)。會把該對應(yīng)的事件函數(shù)綁定到 instance._x 上面來。我們接下來再看下 我們的 create()函數(shù)。
create()函數(shù)代碼如下所示:
create(options) {
this.init(options);
switch(this.options.type) {
case "sync":
return new Function(this.args(), "\"use strict\";\n" + this.header() + this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
onDone: () => "",
rethrowIfPossible: true
}));
case "async":
return new Function(this.args({
after: "_callback"
}), "\"use strict\";\n" + this.header() + this.content({
onError: err => `_callback(${err});\n`,
onResult: result => `_callback(null, ${result});\n`,
onDone: () => "_callback();\n"
}));
case "promise":
let code = "";
code += "\"use strict\";\n";
code += "return new Promise((_resolve, _reject) => {\n";
code += "var _sync = true;\n";
code += this.header();
code += this.content({
onError: err => {
let code = "";
code += "if(_sync)\n";
code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));\n`;
code += "else\n";
code += `_reject(${err});\n`;
return code;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => "_resolve();\n"
});
code += "_sync = false;\n";
code += "});\n";
return new Function(this.args(), code);
}
}
/**
* @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options
*/
init(options) {
this.options = options;
this._args = options.args.slice();
}
如上代碼,其中我們的 create(options) 函數(shù)中的參數(shù) options 的值為如下:
options = {
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
}
而我們的type值為 'sync', 因此會進(jìn)入case語句中的第一個case,該create函數(shù)內(nèi)部,判斷三種類型的情況,分別為 'sync', 'async', 'promise'.
如上代碼 options 對象中的參數(shù):taps 是我們的注冊事件對象的數(shù)組,interceptors 是過濾器,目前是 []; 我們的demo里面沒有使用過濾器,當(dāng)然我們也可以使用過濾器,args 參數(shù)值為 ['xxx']; 我們初始化實列的時候 傳遞了該值;比如如下初始化該類代碼:const h1 = new SyncHook(['xxx']); 然后我們的type為 'sync' 了 。因為我們 h1實列調(diào)用的是call這個方法。搞清楚了上面各個參數(shù)的含義,我們接下來往下看。
在create()方法內(nèi)部,我們首先會調(diào)用 init() 方法,如下代碼所示:this.init(options); 在init內(nèi)部代碼中,
init(options) {
this.options = options;
this._args = options.args.slice();
}
this.options = options, 保存了該對象的引用。this._args = options.args.slice(); 保存了該數(shù)組傳遞進(jìn)來的參數(shù)。
現(xiàn)在就會直接 case 'sync' 的情況了,如下代碼所示:
case "sync":
return new Function(this.args(), "\"use strict\";\n" + this.header() + this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
onDone: () => "",
rethrowIfPossible: true
}));
就會依次調(diào)用 this.args(); this.header(); this.content() 方法; 在 new Function(); 中如何調(diào)用方法看如下代碼來理解,如下圖所示:

下面我們來看下 header() 方法如下代碼所示:
header() {
let code = "";
// this.needContext() 判斷數(shù)組this.taps的某一項是否有 context屬性,任意一項有的話,就返回true
if(this.needContext()) {
// 如果為true的話,var _context = {};
code += "var _context = {};\n";
} else {
// 否則的話, var _context; 值為undefined
code += "var _context;\n";
}
/*
在setup()中,我們把所有的tap對象都給到了 instance, 因此這里的 this._x 就是我們之前說的 instance._x;
*/
code += "var _x = this._x;\n";
// 如果有攔截器的話,保存攔截器數(shù)組到局部變量 _interceptors 中,且數(shù)組保存到 _taps中。
if(this.options.interceptors.length > 0) {
code += "var _taps = this.taps;\n";
code += "var _interceptors = this.interceptors;\n";
}
/*
如果有攔截器的話,遍歷。
獲取到某一個攔截器 const interceptor = this.options.interceptors[i];
如果該攔截器有call這個方法的話,就拼接字符串。因此如果有過濾器的話,最終會拼接成如下字符串:
"use strict";
function(options) {
var _context;
var _x = this._x;
var _taps = this.taps;
var _interceptors = this.interceptors;
// 下面就是循環(huán)攔截器,如果有一個攔截器的話
_interceptors[0].call(options);
}
*/
for(let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if(interceptor.call) {
code += `${this.getInterceptor(i)}.call(${this.args({
before: interceptor.context ? "_context" : undefined
})});\n`;
}
}
return code;
}
needContext() {
for(const tap of this.options.taps)
if(tap.context) return true;
return false;
}
getInterceptor(idx) {
return `_interceptors[${idx}]`;
}
首先我們來看下 needContext() 函數(shù),該函數(shù)遍歷 this.options.taps;它是我們傳進(jìn)來的對象。this.options.taps 值如下:
this.options.taps = [
{type: 'sync', fn: fn, name: 'F', before: 'D'},
{type: 'sync', fn: fn, name: 'A'},
{type: 'sync', fn: fn, name: 'B'},
{type: 'sync', fn: fn, name: 'E', before: 'C'},
{type: 'sync', fn: fn, name: 'C'},
{type: 'sync', fn: fn, name: 'D'}
];
如果該數(shù)組中的某一個對象有 context 屬性的話(該數(shù)組中任意一項),否則都沒有context屬性的話,會返回false。
如上代碼:
if(this.needContext()) {
code += "var _context = {};\n";
} else {
code += "var _context;\n";
}
code += "var _x = this._x;\n";
如果 this.needContext() 為true的話,var _context = {}; 否則的話 var _context; 值為undefined; 因此如果this.needContext() 返回true的話,code的值變?yōu)槿缦滤荆?/p>

如果this.needContext()方法返回false的話,就返回如下所示的值:

我們現(xiàn)在再來看看 this.content() 方法,content()方法并不在我們的HookCodeFactory類中,它是子類自己實現(xiàn)的,因此我們到 SyncHook類中去看代碼如下所示:
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
我們再結(jié)合 HookCodeFactory.js類中,看create()函數(shù)的代碼:
case "sync":
return new Function(this.args(), "\"use strict\";\n" + this.header() + this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
onDone: () => "",
rethrowIfPossible: true
}));
如上代碼,我們的content方法中傳的參數(shù)為一個對象;
{
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
onDone: () => "",
rethrowIfPossible: true
}
因此上面的 SyncHookCodeFactory 類 繼承了 HookCodeFactory 中對應(yīng)的參數(shù)為:
onError = err => `throw ${err};\n`;
onResult = result => `return ${result};\n`;
onDone = () => "";
rethrowIfPossible = true;
如上 onError, onResult, onDone 都是一個函數(shù),然后返回不同的值。最后我們調(diào)用 callTapsSeries 方法來執(zhí)行; 下面我們來看下該 callTapsSeries 方法;方法在 HookCodeFactory 類中,代碼如下所示:
callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {
if(this.options.taps.length === 0)
return onDone();
const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
const next = i => {
if(i >= this.options.taps.length) {
return onDone();
}
const done = () => next(i + 1);
const doneBreak = (skipDone) => {
if(skipDone) return "";
return onDone();
}
return this.callTap(i, {
onError: error => onError(i, error, done, doneBreak),
onResult: onResult && ((result) => {
return onResult(i, result, done, doneBreak);
}),
onDone: !onResult && (() => {
return done();
}),
rethrowIfPossible: rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
});
};
return next(0);
}
如上代碼:if(this.options.taps.length === 0) { return onDone(); } 的含義:如果 taps 處理完畢后或一個taps的長度都沒有的話,就執(zhí)行 onDone 方法,返回一個空字符串。
const firstAsync = this.options.taps.findIndex(t => t.type !== "sync"); 如果第一個異步的下標(biāo)index. 通過t.type !== 'sync' 來判斷,如果沒有異步的話,就返回 -1; findIndex的使用方式如下所示:

下面我們來看這個函數(shù)調(diào)用,如下next方法如下所示:
const next = i => {
if(i >= this.options.taps.length) {
return onDone();
}
const done = () => next(i + 1);
const doneBreak = (skipDone) => {
if(skipDone) return "";
return onDone();
}
return this.callTap(i, {
onError: error => onError(i, error, done, doneBreak),
onResult: onResult && ((result) => {
return onResult(i, result, done, doneBreak);
}),
onDone: !onResult && (() => {
return done();
}),
rethrowIfPossible: rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
});
};
return next(0);
默認(rèn)情況下,我們看到 i = 0; 開始傳遞參數(shù)進(jìn)去,如果 i 大于我們的 注冊事件函數(shù)的 this.taps的數(shù)組的話,就直接返回 我們上面的 onDone()方法。如果不大于,就定義 done 函數(shù),依次遞歸調(diào)用該next()函數(shù),注意我們這邊的 done 函數(shù)目前還沒有被執(zhí)行到。只是定義了一個 done函數(shù)方法放在這里,接下來就是我們的 doneBreak 函數(shù)了,它接收一個參數(shù)為 skipDone;如果有該參數(shù)的話,直接返回空字符串,否則的話,返回調(diào)用 onDone() 方法。最關(guān)鍵的一步在最后,最后我們返回了 this.callTap 這個函數(shù),也就是說,我們的 callTapsSeries 函數(shù)方法的返回值決定于 callTap 這個方法的返回值。之前我們定義了 done() 函數(shù)遞歸調(diào)用及 定義了 doneBreak 函數(shù)都是為 callTap 函數(shù)做準(zhǔn)備的。callTap函數(shù)接收2個參數(shù),第一個參數(shù)為 i 值,第二個參數(shù)為一個對象,該對象定義了 onError,onResult 及 onDone,rethrowIfPossible 函數(shù)。
callTap 函數(shù)變成如下:
this.callTap(i, {
onError: function(error) {
onError(i, error, done, doneBreak);
},
onResult: onResult && function(result) {
return onResult(i, result, done, doneBreak);
},
onDone: !onResult && function() {
return done();
},
rethrowIfPossible: rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
});
如上就是callTap函數(shù)的最終形式了,如果有onResult的話,就會返回一個匿名函數(shù)function, 然后我們調(diào)用該函數(shù)onResult即可。
如果我們沒有 onResult 函數(shù)話,那么我們可以調(diào)用 done函數(shù),該done函數(shù)我們上面定義了如下代碼:const done = () => next(i + 1); 因此如果我們沒有傳遞onResult函數(shù)的話,它會依次循環(huán)我們之前使用 this.taps保存的所有事件,然后依次循環(huán)該事件 對應(yīng)的回調(diào)函數(shù)。就好比我們下面的demo一樣當(dāng)我們調(diào)用 call 方法后,它會依次調(diào)用該回調(diào)函數(shù),然后輸出信息出來,如下demo:
const { SyncHook } = require('tapable');
const h1 = new SyncHook(['xxx']);
h1.tap('A', function(args) {
console.log('A', args);
return 'b';
});
h1.tap('B', function() {
console.log('b');
});
h1.tap('C', function() {
console.log('c');
});
h1.tap({
name: 'F',
before: 'D'
}, function() {
});
h1.tap({
name: 'E',
before: 'C'
}, function() {
});
h1.tap('D', function() {
console.log('d');
});
h1.call(7777);
如上依次輸出 A 7777 b c d
我們再看看 rethrowIfPossible: rethrowIfPossible && (firstAsync < 0 || i < firstAsync) rethrowIfPossible 默認(rèn)返回true,因此會執(zhí)行后面的語句,如果我們的注冊事件中有異步函數(shù)的話,那么我們的 firstAsync 參數(shù)就會返回該異步函數(shù)的索引值,因為我們上面的demo,注冊事件沒有異步函數(shù),因此我們的 firstAsync 返回的值是 -1; 因此 -1 < 0; 因此返回true;后面的 i < firstAsync; 看不看無所謂,因為這里使用了 || 這個語句符。當(dāng)然如果我們注冊事件中有異步函數(shù)的話,那么我們就會繼續(xù) 判斷 i < firstAsync 這個語句了。如果rethrowIfPossible 是false的話,那么當(dāng)前的鉤子函數(shù)的類型就不是 sync,可能是Async或promise類型了。
下面我們來看下 callTap 函數(shù),代碼如下所示:
/*
tapIndex 是下標(biāo)索引。
onError: onError(i, error, done, doneBreak);
onResult:undefined, 因為上面調(diào)用的時候 沒有 onResult 這個參數(shù),所以返回undefined
onDone: done(); 會遞歸調(diào)用我們上面的 next() 函數(shù)。
rethrowIfPossible:默認(rèn)為true.
如果為false的話,說明當(dāng)前的鉤子不是 sync,如果為true的話,說明當(dāng)前的鉤子函數(shù)是 Async 或 Promise
*/
callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
let code = "";
let hasTapCached = false;
// 遍歷攔截器,如果有攔截器的話,如果有就執(zhí)行攔截器的tap函數(shù)
for(let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if(interceptor.tap) {
if(!hasTapCached) {
/*
如下代碼,我們調(diào)用 this.getTap(tapIndex)方法后,會生成 `var _tap[0] = _tap[0]` 等這樣的字符串。
生成完成后,我們設(shè)置 hasTapCached 為true。如果有多個攔截器的話,我們也會執(zhí)行一次。
注意:我們這邊獲取 _taps 對象的下標(biāo)是使用我們傳進(jìn)來的參數(shù) tapIndex。在for循環(huán)中,我們的tapIndex值不會改變的
。
*/
code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
hasTapCached = true;
}
/*
下面的代碼返回的是:code += `_interceptors[0].tap(_tap0)`;
首先會判斷該攔截器是否有 context 這個屬性,如果有的話就獲取 _context 這個屬性,否則的話就空字符串。
*/
code += `${this.getInterceptor(i)}.tap(${interceptor.context ? "_context, " : ""}_tap${tapIndex});\n`;
}
}
/*
下面的代碼返回了:
code += `var _fn0 = _x[0]`
*/
code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
// 獲取 this.taps的索引,獲取第一個或第n個
const tap = this.options.taps[tapIndex];
// 判斷類型,是否是 sync, Async 及 Promise 對象的
/*
rethrowIfPossible 默認(rèn)為true,同步執(zhí)行,如果有異步的話,rethrowIfPossible 返回false,就執(zhí)行if語句代碼,
因此代碼 code += `var _hasError0 = false`; code += "try { \n" 這樣的,如果是異步的話,因為要保證異步順序的
問題,因此這邊使用了 try catch 這樣的語句,防止報錯發(fā)生。
*/
switch(tap.type) {
case "sync":
if(!rethrowIfPossible) {
code += `var _hasError${tapIndex} = false;\n`;
code += "try {\n";
}
/*
判斷 onResult 是否為true還是false,
如果為true的話,那么 code += `var _result0 = _fn0(options)`
如果為false的話,code += `_fn0(options)`; 這樣的方法調(diào)用
*/
if(onResult) {
code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined
})});\n`;
} else {
code += `_fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined
})});\n`;
}
// 把catch語句拼接上
if(!rethrowIfPossible) {
code += "} catch(_err) {\n";
code += `_hasError${tapIndex} = true;\n`;
code += onError("_err");
code += "}\n";
code += `if(!_hasError${tapIndex}) {\n`;
}
// 有 onResult 的話,code += onResult(`_result0`); 就調(diào)用該方法執(zhí)行。這邊是字符串拼接。
if(onResult) {
code += onResult(`_result${tapIndex}`);
}
// 如果有 onDone() 方法的話,就開始遞歸調(diào)用。我們之前有 next(i+1); 這樣的遞歸。
if(onDone) {
code += onDone();
}
if(!rethrowIfPossible) {
code += "}\n";
}
/*
因此如果我們注冊的是同步事件的話,那么我們的最終代碼就變成如下:
var _tap[0] = _tap[0];
_interceptors[0].tap(_tap0);
var _fn0 = _x[0];
_fn0(options);
如果我們的this.taps 有多個同步事件的話,會依次類推... 因此會有如下這樣的:
var _tap[0] = _tap[0];
_interceptors[0].tap(_tap0);
var _fn0 = _x[0];
_fn0(options);
var _tap[1] = _tap[1];
_interceptors[1].tap(_tap1);
var _fn1 = _x[1];
_fn0(options);
..... 依次類推
*/
break;
case "async":
let cbCode = "";
if(onResult)
cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;
else
cbCode += `_err${tapIndex} => {\n`;
cbCode += `if(_err${tapIndex}) {\n`;
cbCode += onError(`_err${tapIndex}`);
cbCode += "} else {\n";
if(onResult) {
cbCode += onResult(`_result${tapIndex}`);
}
if(onDone) {
cbCode += onDone();
}
cbCode += "}\n";
cbCode += "}";
code += `_fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined,
after: cbCode
})});\n`;
break;
case "promise":
code += `var _hasResult${tapIndex} = false;\n`;
code += `_fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined
})}).then(_result${tapIndex} => {\n`;
code += `_hasResult${tapIndex} = true;\n`;
if(onResult) {
code += onResult(`_result${tapIndex}`);
}
if(onDone) {
code += onDone();
}
code += `}, _err${tapIndex} => {\n`;
code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;
code += onError(`_err${tapIndex}`);
code += "});\n";
break;
}
return code;
}
getTap(idx) {
return `_taps[${idx}]`;
}
getInterceptor(idx) {
return `_interceptors[${idx}]`;
}
getTapFn(idx) {
return `_x[${idx}]`;
}
因此我們這邊同步事件在 SyncHook 類中的 compile方法:
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
在返回代碼之前,我們還是整理下整個思路吧,我們首先在 Hook類中代碼如下:
class Hook {
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
}
在我們的子類 SyncHook中重寫了 compile 該方法,代碼如下:
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
因此我們會調(diào)用 setup該方法,代碼如下:
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
最后我們會調(diào)用 factory.create(options); 這句代碼,因此會調(diào)用 HookCodeFactory.js 代碼中的 create()方法。
該方法判斷了三種類型,分別為 sync, Async, promise 等。因為我們這邊都是同步事件,因此會調(diào)用 sync 這個case情況。
因此我們最終代碼返回變成如下:
"use strict"
function(options) {
// 我們首先執(zhí)行 HookCodeFactory類中的 header() 方法生成代碼
var _context;
var _x = this._x;
// 如果我們有攔截器的話,下面代碼也會生成的,如果沒有就忽略下面三句代碼:
var _taps = this.taps;
var _interceptors = this.interceptors;
/*
如果我們只有一個攔截器的話,只會生成一個,如果我們有多個的話,就會使用for循環(huán)生成多個
*/
_interceptors[0].call(options);
// 下面就是我們的callTap函數(shù)返回的代碼了
var _tap[0] = _tap[0];
_interceptors[0].tap(_tap0);
var _fn0 = _x[0];
_fn0(options);
var _tap[1] = _tap[1];
_interceptors[1].tap(_tap1);
var _fn1 = _x[1];
_fn0(options);
..... 依次類推
}
如上差不多就是一整個同步事件的流程了。至于其他的異步Async 和 promise,大家有空可以去折騰一下,里面的邏輯有點多。
按照上面的我們思路也可以簡單的折騰下了。我們可以看到,我們上面的同步事件的demo是如何被執(zhí)行的,及它的回調(diào)函數(shù)是什么時候被執(zhí)行的。當(dāng)然里面還有很多參數(shù)判斷,我們可以去根據(jù)API文檔或他們寫的測試用例去理解下應(yīng)該差不多了。