上一篇文章《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。markdown
分析:tap和call方法確定是要有的,不在這裏,那就在它的基類Hook裏。這裏使用到了繼承和工廠模式,咱們能夠經過源碼學習它們的實踐了。閉包
咱們不急着看Hook.js,既然它用到繼承,就是將公共的、可複用的邏輯抽象到父類中了。若是直接看父類,咱們可能不容易發現做者抽象的思路,爲何要將這些點抽象到父類中。異步
咱們先看看這些繼承了Hook的子類,看看它們有那些公共的地方,再去看父類Hook.js。async
// 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全都相似,只是使用的工廠不一樣。ide
分析:仍是分析不出什麼,同步的鉤子看完了,接着在看異步鉤子類。函數
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的核心邏輯,就研究完畢了,感興趣的小夥伴能夠繼續再看看。能夠看到源碼中對於面向對象繼承的使用,工廠模式的使用,調用時才生成執行邏輯這種操做。都是值得咱們學習的。