Webpack tapable 源碼研究

研究tapable源碼的理由

  1. 大佬寫的代碼,固然值得一看了。
  2. tapable源碼的代碼量夠少,可讓咱們花少許時間就能研究的明白,還能有所收穫。

上一篇文章《Webpack tapable 使用研究》研究了tapable的用法,瞭解用法有助於咱們理解源碼。感興趣能夠看看。api

查看SyncHook.js文件

看源碼,第一感受確定是充滿疑惑的。數組

先從用法最簡單的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裏。這裏使用到了繼承和工廠模式,咱們能夠經過源碼學習它們的實踐了。閉包

查看SyncBailHook.js、SyncLoopHook.js、SyncWaterfallHook.js文件

咱們不急着看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

分析:仍是分析不出什麼,同步的鉤子看完了,接着在看異步鉤子類。函數

查看AsyncParallelHook.js文件

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方法究竟是幹啥的?

查看Hook.js

帶着疑惑,咱們來看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是什麼關係?

咱們接着深刻的看。

註冊過程:tap、tapAsync、tapPromise方法

既然調用方法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方法

intercept方法將攔截器的相應回調放到interceptors裏,以供對應的時機調用。

調用過程: call方法、callAsync方法、promise方法

註冊過程機會沒什麼區別,區別在於調用過程,最終影響插件的執行順序和邏輯。

首先先解決爲何_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);
複製代碼

打印結果以下:

image

能夠看到咱們的call方法中的x就是setup方法中設置的咱們插件的回調函數啊,call方法生成的代碼,就是根據咱們使用不一樣的鉤子,根據咱們設計的邏輯,調用這些回調。

在看一下hook對象下的call和callAsync有何不一樣,callAsync沒有被調用,因此它仍是lazyCompileHook函數,也驗證了咱們的思考,call方法是在調用時,才被生成了上面那樣的執行函數。

結束語

tapable的核心邏輯,就研究完畢了,感興趣的小夥伴能夠繼續再看看。能夠看到源碼中對於面向對象繼承的使用,工廠模式的使用,調用時才生成執行邏輯這種操做。都是值得咱們學習的。

相關文章
相關標籤/搜索