作者:崔靜
上一篇總覽 我們介紹了 webpack 整體的編譯過程,這次就來分析下基礎的 Tapable。
介紹
webpack 整個編譯過程中暴露出來大量的 Hook 供內部/外部外掛使用,同時支援擴充套件各種外掛,而內部處理的程式碼,也依賴於 Hook 和外掛,這部分的功能就依賴於 Tapable。webpack 的整體執行過程,總的來看就是事件驅動的。從一個事件,走向下一個事件。Tapable 用來提供各種型別的 Hook。我們通過下面一個直觀的使用例子,初步認識一下 Tapable:
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 型別。
下圖展示了每種型別的作用:
-
BasicHook: 執行每一個,不關心函式的返回值,有 SyncHook、AsyncParallelHook、AsyncSeriesHook。
我們平常使用的 eventEmit 型別中,這種型別的鉤子是很常見的。
-
BailHook: 順序執行 Hook,遇到第一個結果 result !== undefined 則返回,不再繼續執行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。
什麼樣的場景下會使用到 BailHook 呢?設想如下一個例子:假設我們有一個模組 M,如果它滿足 A 或者 B 或者 C 三者任何一個條件,就將其打包為一個單獨的。這裡的 A、B、C 不存在先後順序,那麼就可以使用 AsyncParallelBailHook 來解決:
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 的實現差異:
//syncHookclass SyncHookCodeFactory extends HookCodeFactory {
content({
onError, onResult, onDone, rethrowIfPossible
}) {
return this.callTapsSeries({
onError: (i, err) =>
onError(err), onDone, rethrowIfPossible
});
}
}//syncBailHookcontent({
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
});
}//AsyncSeriesLoopHookclass 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 ,上面程式碼翻譯成圖,如下
- 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 _loopdo {
_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 來說,由於其整體是由事件機制推動,內部存在大量這樣的邏輯。那麼這種拆開來直接執行每一個函式的方式,便可看出其優勢所在。