做者:崔靜webpack
上一篇總覽 咱們介紹了 webpack 總體的編譯過程,此次就來分析下基礎的 Tapable。git
webpack 整個編譯過程當中暴露出來大量的 Hook 供內部/外部插件使用,同時支持擴展各類插件,而內部處理的代碼,也依賴於 Hook 和插件,這部分的功能就依賴於 Tapable。webpack 的總體執行過程,總的來看就是事件驅動的。從一個事件,走向下一個事件。Tapable 用來提供各類類型的 Hook。咱們經過下面一個直觀的使用例子,初步認識一下 Tapable:github
const {
SyncHook
} = require('tapable')
// 建立一個同步 Hook,指定參數
const hook = new SyncHook(['arg1', 'arg2'])
// 註冊
hook.tap('a', function (arg1, arg2) {
console.log('a')
})
hook.tap('b', function (arg1, arg2) {
console.log('b')
})
hook.call(1, 2)
複製代碼
看起來起來功能和 EventEmit 相似,先註冊事件,而後觸發事件。不過 Tapable 的功能要比 EventEmit 強大。從官方介紹中,能夠看到 Tapable 提供了不少類型的 Hook,分爲同步和異步兩個大類(異步中又區分異步並行和異步串行),而根據事件執行的終止條件的不一樣,由衍生出 Bail/Waterfall/Loop 類型。web
下圖展現了每種類型的做用:promise
BasicHook: 執行每個,不關心函數的返回值,有 SyncHook、AsyncParallelHook、AsyncSeriesHook。bash
咱們日常使用的 eventEmit 類型中,這種類型的鉤子是很常見的。併發
BailHook: 順序執行 Hook,遇到第一個結果 result !== undefined 則返回,再也不繼續執行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。異步
什麼樣的場景下會使用到 BailHook 呢?設想以下一個例子:假設咱們有一個模塊 M,若是它知足 A 或者 B 或者 C 三者任何一個條件,就將其打包爲一個單獨的。這裏的 A、B、C 不存在前後順序,那麼就可使用 AsyncParallelBailHook 來解決:async
x.hooks.拆分模塊的Hook.tap('A', () => {
if (A 判斷條件知足) {
return true
}
})
x.hooks.拆分模塊的Hook.tap('B', () => {
if (B 判斷條件知足) {
return true
}
})
x.hooks.拆分模塊的Hook.tap('C', () => {
if (C 判斷條件知足) {
return true
}
})
複製代碼
若是 A 中返回爲 true,那麼就無須再去判斷 B 和 C。 可是當 A、B、C 的校驗,須要嚴格遵循前後順序時,就須要使用有順序的 SyncBailHook(A、B、C 是同步函數時使用) 或者 AsyncSeriseBailHook(A、B、C 是異步函數時使用)。函數
WaterfallHook: 相似於 reduce,若是前一個 Hook 函數的結果 result !== undefined,則 result 會做爲後一個 Hook 函數的第一個參數。既然是順序執行,那麼就只有 Sync 和 AsyncSeries 類中提供這個Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook
當一個數據,須要通過 A,B,C 三個階段的處理獲得最終結果,而且 A 中若是知足條件 a 就處理,不然不處理,B 和 C 一樣,那麼可使用以下
x.hooks.tap('A', (data) => {
if (知足 A 須要處理的條件) {
// 處理數據 data
return data
} else {
return
}
})
x.hooks.tap('B', (data) => {
if (知足B須要處理的條件) {
// 處理數據 data
return data
} else {
return
}
})
x.hooks.tap('C', (data) => {
if (知足 C 須要處理的條件) {
// 處理數據 data
return data
} else {
return
}
})
複製代碼
LoopHook: 不停的循環執行 Hook,直到全部函數結果 result === undefined。一樣的,因爲對串行性有依賴,因此只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暫時沒看到具體使用 Case)
咱們先給出 Tapable 代碼的主脈絡:
hook 事件註冊 ——> hook 觸發 ——> 生成 hook 執行代碼 ——> 執行
hook 類關係圖很簡單,各類 hook 都繼承自一個基本的 Hook 抽象類,同時內部包含了一個 xxxCodeFactory 類,會在生成 hook 執行代碼中用到。
Tapable 基本邏輯是,先經過類實例的 tap 方法註冊對應 Hook 的處理函數:
Tapable 提供了 tap/tapAsync/tapPromise 這三個註冊事件的方法(實現邏輯在 Hook 基類中),分別針對同步(tap)/異步(tapAsync/tapPromise),對要 push 到 taps 中的內容賦給不同的 type 值,如上圖所示。
對於 SyncHook, SyncBailHook, SyncLoopHook, SyncWaterfallHook 這四個同步類型的 Hook, 則會覆寫基類中 tapAsync 和 tapPromise 方法,防止使用者在同步 Hook 中誤用異步方法。
tapAsync() {
throw new Error("tapAsync is not supported on a SyncHook");
}
tapPromise() {
throw new Error("tapPromise is not supported on a SyncHook");
}
複製代碼
與 tap/tapAsync/tapPromise 相對應的,Tapable 中提供了三種觸發事件的方法 call/callAsync/promise。這三這方法也位於基類 Hook 中,具體邏輯以下
this.call = this._call = this._createCompileDelegate("call", "sync");
this.promise = this._promise = this._createCompileDelegate("promise", "promise");
this.callAsync = this._callAsync = this._createCompileDelegate("callAsync", "async");
// ...
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
_createCompileDelegate(name, type) {
const lazyCompileHook = (...args) => {
this[name] = this._createCall(type);
return this[name](...args);
};
return lazyCompileHook;
}
複製代碼
不管是 call, 仍是 callAsync 和 promise,最終都會調用到 compile
方法,再此以前,其區別就是 compile
中所傳入的 type
值的不一樣。而 compile
根據不一樣的 type
類型生成了一個可執行函數,而後執行該函數。
注意上面代碼中有一個變量名稱 lazyCompileHook,懶編譯。當咱們 new Hook 的時候,其實會先生成了 promise, call, callAsync 對應的 CompileDelegate 代碼,其實際的結構是
this.call = (...args) => {
this[name] = this._createCall('sync');
return this['call'](...args);
}
this.promise = (...args) => {
this[name] = this._createCall('promise');
return this['promise'](...args);
}
this.callAsync = (...args) => {
this[name] = this._createCall('async');
return this['callAsync'](...args);
}
複製代碼
當在觸發 hook 時,好比執行 xxhook.call()
時,纔會編譯出對應的執行函數。這個過程就是所謂的「懶編譯」,即用的時候才編譯,已達到最優的執行效率。
接下來咱們主要看 compile
的邏輯,這塊也是 Tapable 中大部分的邏輯所在。
在看源碼以前,咱們能夠先寫幾個簡單的 demo,看一下 Tapable 最終生成了什麼樣的執行代碼,來直觀感覺一下:
上圖分別是 SyncHook.call, AsyncSeriesHook.callAsync 和 AsyncSeriesHook.promise 生成的代碼。_x
中保存了註冊的事件函數,_fn${index}
則是每個函數的執行,而生成的代碼中根據不一樣的 Hook 以及以不一樣的調用方式, _fn${index}
會有不一樣的執行方式。這些差別是如何經過代碼生成的呢?咱們來細看 compile
方法。
compile
這個方法在基類中並無實現,其實現位於派生出來的各個類中。以 SyncHook 爲例,看一下
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 {
// ... 省略其餘代碼
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
複製代碼
這裏生成可執行代碼使用了工廠模式:HookCodeFactory
是一個用來生成代碼的工廠基類,每個 Hook 中派生出一個子類。全部的 Hook 中 compile 都調用到了 create 方法。先來看一下這個 create 方法作了什麼。
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);
}
}
複製代碼
乍一看代碼有點多,簡化一下,畫個圖,就是下面的流程:
由此能夠看到,create 中只實現了代碼的主模板,實現了公共的部分(函數參數和函數一開始的公共參數),而後留出差別的部分 content
,交給各個子類來實現。而後橫向對比一下各個 Hook 中繼承自 HookCodeFactory 的子 CodeFactory,看一下 content 的實現差別:
//syncHook
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
//syncBailHook
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onResult: (i, result, next) => `if(${result} !== undefined) {\n${onResult(result)};\n} else {\n${next()}}\n`,
onDone,
rethrowIfPossible
});
}
//AsyncSeriesLoopHook
class AsyncSeriesLoopHookCodeFactory extends HookCodeFactory {
content({ onError, onDone }) {
return this.callTapsLooping({
onError: (i, err, next, doneBreak) => onError(err) + doneBreak(true),
onDone
});
}
}
// 其餘的結構都相似,便不在這裏貼代碼了
複製代碼
能夠看到,在全部的子類中,都實現了 content
方法,根據不一樣鉤子執行流程的不一樣,調用了 callTapsSeries/callTapsParallel/callTapsLooping
而且會有 onError, onResult, onDone, rethrowIfPossible
這四中狀況下的代碼片斷。
callTapsSeries/callTapsParallel/callTapsLooping
都在基類的方法中,這三個方法中都會走到一個 callTap 的方法。先看一下 callTap 方法。代碼比較長,不想看代碼的能夠直接看後面的圖。
callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
let code = "";
let hasTapCached = false;
// 這裏的 interceptors 先忽略
for(let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if(interceptor.tap) {
if(!hasTapCached) {
code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
hasTapCached = true;
}
code += `${this.getInterceptor(i)}.tap(${interceptor.context ? "_context, " : ""}_tap${tapIndex});\n`;
}
}
code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
const tap = this.options.taps[tapIndex];
switch(tap.type) {
case "sync":
if(!rethrowIfPossible) {
code += `var _hasError${tapIndex} = false;\n`;
code += "try {\n";
}
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`;
}
if(!rethrowIfPossible) {
code += "} catch(_err) {\n";
code += `_hasError${tapIndex} = true;\n`;
code += onError("_err");
code += "}\n";
code += `if(!_hasError${tapIndex}) {\n`;
}
if(onResult) {
code += onResult(`_result${tapIndex}`);
}
if(onDone) {
code += onDone();
}
if(!rethrowIfPossible) {
code += "}\n";
}
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;
}
複製代碼
也是對應的分紅 sync/async/promise ,上面代碼翻譯成圖,以下
總的來看, callTap 內是一次函數執行的模板,也是根據調用方式的不一樣,分爲 sync/async/promise 三種。
而後看 callTapsSeries 方法,
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 和 onDone 的判斷條件,就是說有 onResult 或者 onDone
onResult: onResult && ((result) => {
return onResult(i, result, done, doneBreak);
}),
onDone: !onResult && (() => {
return done();
}),
rethrowIfPossible: rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
});
};
return next(0);
}
複製代碼
注意看 this.callTap 中 onResult 和 onDone 的條件,就是說要麼執行 onResult, 要麼執行 onDone。先看簡單的直接走 onDone 的邏輯。那麼結合上面 callTap 的流程,以 sync 爲例,能夠獲得下面的圖:
對於這種狀況,callTapsSeries 的結果是遞歸的生成每一次的調用 code,直到最後一個時,直接調用外部傳入的 onDone 方法獲得結束的 code, 遞歸結束。而對於執行 onResult 的流程,看一下 onResult 代碼: return onResult(i, result, done, doneBreak)
。簡單理解,和上面圖中流程同樣的,只不過在 done 的外面用 onResult 包裹了一層關於 onResult 的邏輯。
接着咱們看 callTapsLooping 的代碼:
callTapsLooping({ onError, onDone, rethrowIfPossible }) {
if(this.options.taps.length === 0)
return onDone();
const syncOnly = this.options.taps.every(t => t.type === "sync");
let code = "";
if(!syncOnly) {
code += "var _looper = () => {\n";
code += "var _loopAsync = false;\n";
}
// 在代碼開始前加入 do 的邏輯
code += "var _loop;\n";
code += "do {\n";
code += "_loop = false;\n";
// interceptors 先忽略,只看主要部分
for(let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if(interceptor.loop) {
code += `${this.getInterceptor(i)}.loop(${this.args({ before: interceptor.context ? "_context" : undefined })});\n`;
}
}
code += this.callTapsSeries({
onError,
onResult: (i, result, next, doneBreak) => {
let code = "";
code += `if(${result} !== undefined) {\n`;
code += "_loop = true;\n";
if(!syncOnly)
code += "if(_loopAsync) _looper();\n";
code += doneBreak(true);
code += `} else {\n`;
code += next();
code += `}\n`;
return code;
},
onDone: onDone && (() => {
let code = "";
code += "if(!_loop) {\n";
code += onDone();
code += "}\n";
return code;
}),
rethrowIfPossible: rethrowIfPossible && syncOnly
})
code += "} while(_loop);\n";
if(!syncOnly) {
code += "_loopAsync = true;\n";
code += "};\n";
code += "_looper();\n";
}
return code;
}
複製代碼
先簡化到最簡單的邏輯就是下面這段,很簡單的 do/while 邏輯。
var _loop
do {
_loop = false
// callTapsSeries 生成中間部分代碼
} while(_loop)
複製代碼
callTapsSeries 前面瞭解了其代碼,這裏調用 callTapsSeries 時,有 onResult 邏輯,也就是說中間部分會生成相似下面的代碼(還是以 sync 爲例)
var _fn${tapIndex} = _x[${tapIndex}];
var _hasError${tapIndex} = false;
try {
fn1(${this.args({
before: tap.context ? "_context" : undefined
})});
} catch(_err) {
_hasError${tapIndex} = true;
onError("_err");
}
if(!_hasError${tapIndex}) {
// onResult 中生成的代碼
if(${result} !== undefined) {
_loop = true;
// doneBreak 位於 callTapsSeries 代碼中
//(skipDone) => {
// if(skipDone) return "";
// return onDone();
// }
doneBreak(true); // 實際爲空語句
} else {
next()
}
}
複製代碼
經過在 onResult 中控制函數執行完成後到執行下一個函數之間,生成代碼的不一樣,就從 callTapsSeries 中衍生出了 LoopHook 的邏輯。
而後咱們看 callTapsParallel
callTapsParallel({ onError, onResult, onDone, rethrowIfPossible, onTap = (i, run) => run() }) {
if(this.options.taps.length <= 1) {
return this.callTapsSeries({ onError, onResult, onDone, rethrowIfPossible })
}
let code = "";
code += "do {\n";
code += `var _counter = ${this.options.taps.length};\n`;
if(onDone) {
code += "var _done = () => {\n";
code += onDone();
code += "};\n";
}
for(let i = 0; i < this.options.taps.length; i++) {
const done = () => {
if(onDone)
return "if(--_counter === 0) _done();\n";
else
return "--_counter;";
};
const doneBreak = (skipDone) => {
if(skipDone || !onDone)
return "_counter = 0;\n";
else
return "_counter = 0;\n_done();\n";
}
code += "if(_counter <= 0) break;\n";
code += onTap(i, () => this.callTap(i, {
onError: error => {
let code = "";
code += "if(_counter > 0) {\n";
code += onError(i, error, done, doneBreak);
code += "}\n";
return code;
},
onResult: onResult && ((result) => {
let code = "";
code += "if(_counter > 0) {\n";
code += onResult(i, result, done, doneBreak);
code += "}\n";
return code;
}),
onDone: !onResult && (() => {
return done();
}),
rethrowIfPossible
}), done, doneBreak);
}
code += "} while(false);\n";
return code;
}
複製代碼
因爲 callTapsParallel 最終生成的代碼是併發執行的,那麼代碼流程就和兩個差別較大。上面代碼看起來較多,捋一下主要結構,其實就是下面的圖(還是以 sync 爲例)
總結一下 callTap 中實現了 sync/promise/async 三種基本的一次函數執行的模板,同時將涉及函數執行流程的代碼 onError/onDone/onResult 部分留出來。而 callTapsSeries/callTapsLooping/callTapsParallel 中,經過傳入不一樣的 onError/onDone/onResult 實現出不一樣流程的模板。不過 callTapsParallel 因爲差別較大,經過在 callTap 外包裹一層 onTap 函數,對生成的結果進行再次加工。
到此,咱們獲得了 series/looping/parallel 三大類基礎模板。咱們注意到,callTapsSeries/callTapsLooping/callTapsParallel 中同時也暴露出了本身的 onError, onResult, onDone, rethrowIfPossible, onTap,由此來實現每一個子 Hook 根據不一樣狀況對基礎模板進行定製。以 SyncBailHook 爲例,它和 callTapsSeries 獲得的基礎模板的主要區別在於函數執行結束時機不一樣。所以對於 SyncBailHook 來講,修改 onResult 便可達到目的:
class SyncBailHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
// 修改一下 onResult,若是 函數執行獲得的 result 不爲 undefined 則直接返回結果,不然繼續執行下一個函數
onResult: (i, result, next) => `if(${result} !== undefined) {\n${onResult(result)};\n} else {\n${next()}}\n`,
onDone,
rethrowIfPossible
});
}
}
複製代碼
最後咱們來用一張圖,總體的總結一下 compile 部分生成最終執行代碼的思路:總結出通用的代碼模板,將差別化部分拆分到函數中而且暴露給外部來實現。
相比於簡單的 EventEmit 來講,Tapable 做爲 webpack 底層事件流庫,提供了豐富的事件。而最終事件觸發後的執行,是先動態生成執行的 code,而後經過 new Function 來執行。相比於咱們平時直接遍歷或者遞歸的調用每個事件來講,這種執行方法效率上來講相對更高效。雖然平時寫代碼時,對於一個循環,是拆開來寫每個仍是直接 for 循環,在效率上來講看不出什麼,可是對 webpack 來講,因爲其總體是由事件機制推進,內部存在大量這樣的邏輯。那麼這種拆開來直接執行每個函數的方式,即可看出其優點所在。