上一篇文章《Webpack tapable 使用研究》研究了tapable的用法,瞭解用法有助於咱們理解源碼。感興趣能夠看看。api
看源碼,第一感受確定是充滿疑惑的。數組
先從用法最簡單的SyncHook來看吧。我想象的SyncHook大體是這樣:promise
export default class SyncHook {
constructor() {
this.taps = [];
}
tap(name, fn) {
this.taps.push({
name,
fn,
});
}
call() {
this.taps.forEach(tap => tap.fn());
}
}
複製代碼
有個tap方法,有個call方法,有個變量存儲註冊的插件,但是實際上不是:bash
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
class SyncHookCodeFactory extends 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);
}
}
module.exports = SyncHook;
複製代碼
沒有tap也沒有call,反而有tapAsync和tapPromise。還有個不知幹啥的compile方法,裏面還用了工廠。SyncHook繼承自Hook。閉包
分析:tap和call方法確定是要有的,不在這裏,那就在它的基類Hook裏。這裏使用到了繼承和工廠模式,咱們能夠經過源碼學習它們的實踐了。異步
咱們不急着看Hook.js,既然它用到繼承,就是將公共的、可複用的邏輯抽象到父類中了。若是直接看父類,咱們可能不容易發現做者抽象的思路,爲何要將這些點抽象到父類中。async
咱們先看看這些繼承了Hook的子類,看看它們有那些公共的地方,再去看父類Hook.js。ide
// SyncBailHook.js
class SyncBailHookCodeFactory extends HookCodeFactory {
...
}
const factory = new SyncBailHookCodeFactory();
class SyncBailHook extends Hook {
tapAsync() {
throw new Error("tapAsync is not supported on a SyncBailHook");
}
tapPromise() {
throw new Error("tapPromise is not supported on a SyncBailHook");
}
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
module.exports = SyncBailHook;
複製代碼
SyncBailHook與SyncHook的區別就是換了個工廠給compile方法。其餘沒有什麼不一樣。SyncLoopHook.js、SyncWaterfallHook.js全都相似,只是使用的工廠不一樣。函數
分析:仍是分析不出什麼,同步的鉤子看完了,接着在看異步鉤子類。oop
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
class AsyncParallelHookCodeFactory extends HookCodeFactory {
...
}
const factory = new AsyncParallelHookCodeFactory();
class AsyncParallelHook extends Hook {
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
Object.defineProperties(AsyncParallelHook.prototype, {
_call: { value: undefined, configurable: true, writable: true }
});
module.exports = AsyncParallelHook;
複製代碼
連tapAsync和tapPromise的異常拋出都沒有了,只剩compile方法了。下面還用Object.defineProperties給還AsyncParallelHook定義了一個_call方法。其餘的異步鉤子類,也跟AsyncParallelHook文件很相似,就是compile中使用的工廠不一樣。將_call的value定義爲null。
分析:這裏用Object.defineProperties定義類方法是個疑惑點,爲何不直接寫在類中,而是用這種方式呢?
再就是說明各個Hook之間的主要區別,在於compile方法,compile方法裏使用的不一樣工廠類,也是主要的區別點。其餘全部邏輯,都抽象到Hook.js裏了。
咱們如今的疑惑,compile方法究竟是幹啥的?
帶着疑惑,咱們來看tapable有着最核心的邏輯的Hook.js文件,先省略一些部分,先看關鍵的api:
class Hook {
constructor(args) {
if (!Array.isArray(args)) args = [];
this._args = args;
this.taps = [];
this.interceptors = [];
this.call = this._call;
this.promise = this._promise;
this.callAsync = this._callAsync;
}
compile(options) {
throw new Error("Abstract: should be overriden");
}
tap(options, fn) {
...
}
tapAsync(options, fn) {
...
}
tapPromise(options, fn) {
...
}
intercept(interceptor) {
...
}
}
Object.defineProperties(Hook.prototype, {
_call: {
value: createCompileDelegate("call", "sync"),
configurable: true,
writable: true
},
_promise: {
value: createCompileDelegate("promise", "promise"),
configurable: true,
writable: true
},
_callAsync: {
value: createCompileDelegate("callAsync", "async"),
configurable: true,
writable: true
}
});
module.exports = Hook;
複製代碼
先看構造函數,接收args的數組,做爲插件的參數標識。taps變量存儲插件,interceptors變量存儲攔截器。
再看方法,compile方法在這,標識是個抽象方法,由子類重寫,也符合咱們查看子類的預期。
tap、tapAsync、tapPromise、intercept在子類中都會被繼承下來,可是在同步的鉤子中,tapAsync、tapPromise被拋了異常了,不能用,也符合使用時的預期。
這裏比較疑惑的是call、promise、callAsync這三個調用方法,爲啥不像tap這樣寫在類裏,而是寫在構造函數的變量裏,並且下面Object.defineProperties定義了三個_call、_promise、_callAsync三個私有方法,它們和call、promise、callAsync是什麼關係?
咱們接着深刻的看。
既然調用方法call、promise、callAsync的實現比較複雜,咱們就先看tap、tapAsync、tapPromise這些註冊方法,實現比較簡單:
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);
}
tapAsync(options, fn) {
if (typeof options === "string") options = { name: options };
if (typeof options !== "object" || options === null)
throw new Error(
"Invalid arguments to tapAsync(options: Object, fn: function)"
);
options = Object.assign({ type: "async", fn: fn }, options);
if (typeof options.name !== "string" || options.name === "")
throw new Error("Missing name for tapAsync");
options = this._runRegisterInterceptors(options);
this._insert(options);
}
tapPromise(options, fn) {
if (typeof options === "string") options = { name: options };
if (typeof options !== "object" || options === null)
throw new Error(
"Invalid arguments to tapPromise(options: Object, fn: function)"
);
options = Object.assign({ type: "promise", fn: fn }, options);
if (typeof options.name !== "string" || options.name === "")
throw new Error("Missing name for tapPromise");
options = this._runRegisterInterceptors(options);
this._insert(options);
}
複製代碼
它們三個的實現很是相似。核心功能是拼起一個options對象,options的內容以下:
options:{
name, // 插件名稱
type: "sync" | "async" | "promise", // 插件註冊的類型
fn, // 插件的回調函數,被call時的響應函數
stage, // 插件調用的順序值
before,// 插件在哪一個插件以前調用
}
複製代碼
拼好了options,就利用_insert方法將其放到taps變量裏,以供後續調用。_insert方法內部就是實現了根據stage和before兩個值,對options的插入到taps中的順序作了調整並插入。
intercept方法將攔截器的相應回調放到interceptors裏,以供對應的時機調用。
註冊過程機會沒什麼區別,區別在於調用過程,最終影響插件的執行順序和邏輯。
首先先解決爲何_call方法要寫成Object.defineProperties中定義,而不是類中定義,這樣的好處是,方便咱們爲_call方法賦值爲另外一個函數,代碼中將_call的value賦值成了createCompileDelegate方法的返回值,而若是將_call直接聲明到類中,很差作到。再就是能夠直接在子類(如AsyncParallelHook)中,再利用Object.defineProperties將_call的vale賦值爲null。就能夠獲得一個沒有_call方法的子類了。
再看一個私有方法:
_resetCompilation() {
this.call = this._call;
this.callAsync = this._callAsync;
this.promise = this._promise;
}
複製代碼
此方法在_insert和intercept中調用,也就是在每次的註冊新插件或註冊新的攔截器,會觸發一次私有調用方法到call等變量的一次賦值。
爲何每次都要從新賦值呢?每次的_call方法不同了嗎?我先給出答案,確實,每次賦值都是一個全新的new出來的_call方法。由於註冊新插件或註冊新的攔截器會造成一個新的_call方法,因此每次都要從新賦值一次。
那爲何要每次生成一個新的_call方法呢?直接寫死很差嗎,不就是調用taps變量裏的插件和攔截器嗎?
緣由是由於咱們的插件彼此有着聯繫,因此咱們用了這麼多類型的鉤子來控制這些聯繫,每次註冊了新的插件或攔截器,咱們就要從新排布插件和攔截器的調用順序,因此每次都要生成新的_call方法。接下來咱們來看代碼:
function createCompileDelegate(name, type) {
return function lazyCompileHook(...args) {
this[name] = this._createCall(type);
return this[name](...args);
};
}
複製代碼
生成_call方法的是createCompileDelegate方法,這裏用到了閉包,存儲了name和type。而後返回了一個lazyCompileHook方法給_call變量。當_call方法被調用時,_createCall方法也當即被調用。
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
複製代碼
這裏調用了compile方法,也就是說咱們的調用方法(call方法、callAsync方法、promise方法)和compile是息息相關的。看SyncHook中的compile
class SyncHookCodeFactory extends HookCodeFactory {
...
}
const factory = new SyncHookCodeFactory();
export default class SyncHook {
...
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
複製代碼
compile關聯了HookCodeFactory,咱們來看HookCodeFactory的setup和create方法都幹了什麼:
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
複製代碼
setup就是將插件的回調函數,都存在鉤子實例的_x變量上。
create(options) {
this.init(options);
let fn;
switch (this.options.type) {
case "sync":
fn = new Function(
this.args(),
'"use strict";\n' +
this.header() +
this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
resultReturns: true,
onDone: () => "",
rethrowIfPossible: true
})
);
break;
...
}
複製代碼
create方法咱們只關注跟Sync相關的,這裏的變量fn就是最終在調用的時刻,生成了一個call方法的執行體。咱們來看一下這個生成的call方法什麼樣:
實驗代碼:
import { SyncHook } from 'tapable';
const hook = new SyncHook(['options']);
hook.tap('A', function (arg) {
console.log('A', arg);
})
hook.tap('B', function () {
console.log('b')
})
hook.call(6);
console.log(hook.call);
console.log(hook);
複製代碼
打印結果以下:
能夠看到咱們的call方法中的x就是setup方法中設置的咱們插件的回調函數啊,call方法生成的代碼,就是根據咱們使用不一樣的鉤子,根據咱們設計的邏輯,調用這些回調。
在看一下hook對象下的call和callAsync有何不一樣,callAsync沒有被調用,因此它仍是lazyCompileHook函數,也驗證了咱們的思考,call方法是在調用時,才被生成了上面那樣的執行函數。
tapable的核心邏輯,就研究完畢了,感興趣的小夥伴能夠繼續再看看。能夠看到源碼中對於面向對象繼承的使用,工廠模式的使用,調用時才生成執行邏輯這種操做。都是值得咱們學習的。