webpack系列之二Tapable

做者:崔靜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

Basic & Bail

Waterfall & Loop

  • 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 uml

事件註冊

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 ,上面代碼翻譯成圖,以下

  • sync 類型:

sync

  • async 類型:

async

  • promise 類型

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 爲例,能夠獲得下面的圖:

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 爲例)

callTapsParallel

總結一下 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 來講,因爲其總體是由事件機制推進,內部存在大量這樣的邏輯。那麼這種拆開來直接執行每個函數的方式,即可看出其優點所在。

相關文章
相關標籤/搜索