tapable源码搬运解读

669 阅读7分钟

总要有个前言

想必看这篇文章的各位总知道tapable是什么,各种讲解文章也很多,这里不赘述,只是放一些传送门。

QA形式介绍了十种Hook的使用和实现方式

放了很多源码,值得一看

讲了Hook的使用和拦截器

源码

version: 2.0.0-beta.11

源码目录

Hook

为便于阅读,只展示主干代码,代码有缩减

const CALL_DELEGATE = function(...args) {};
const CALL_ASYNC_DELEGATE = function(...args) {};
const PROMISE_DELEGATE = function(...args) {};

class Hook {
	constructor(args = [], name = undefined) {
    	// 初始化 this._args, this.name, this.taps, this.interceptors
		this._call = this.call = CALL_DELEGATE;
		this._callAsync = this.callAsync = CALL_ASYNC_DELEGATE;
		this._promise = this.promise = PROMISE_DELEGATE;
		this._x = undefined;
	}
    
	// abstract
	compile(options)

	_createCall(type) { }

	_tap(type, options, fn) {
    	// normalize & merge options 对参数进行标准化和合并
        // 如:_tap('sync', 'PluginName', fn)
        // options => {name: 'PluginName', type: 'sync', fn: fn}
        // 如:_tap('sync', {}, fn)
        // options => {type: 'sync', fn: fn, ...options}
		options = this._runRegisterInterceptors(options);
		this._insert(options);
	}

	tap(options, fn) {
		this._tap("sync", options, fn);
	}

	tapAsync(options, fn) {
		this._tap("async", options, fn);
	}

	tapPromise(options, fn) {
		this._tap("promise", options, fn);
	}

	// 触发拦截器register函数
	_runRegisterInterceptors(options) {
    	// 循环遍历 options = this.interceptors[i].register?.(options) || options
		return options;
	}

	withOptions(options) { }

	isUsed() { }

	intercept(interceptor) {
		this._resetCompilation();
		this.interceptors.push(Object.assign({}, interceptor));
		// 循环遍历
        this.taps[i] = interceptor.register?.(this.taps[i]);
	}

	_resetCompilation() { }

	_insert(item) {
		this._resetCompilation();
        // 根据 item.before 或string, item.stage 将item插入合适的位置
        while(i > 0) { 
        	// 元素element后移
            if(item.stage < element.stage || item.before.has(element.name))
            	continue;
        }
		this.taps[i] = item;
	}
}
Object.setPrototypeOf(Hook.prototype, null);
module.exports = Hook;

tap, tapAsync, tapPromise

我们可以看到,tap tapAsync tapPromise 都是调用了_tap,只是传递了不同的options.type_tap会将options标准化合并,然后触发拦截器interceptor.register,最后将options根据options.stageoptions.before插入到this.taps对应的位置。

  1. 标准化options
  2. 触发拦截器register
  3. _insertthis.taps

call, callAsync, promise

call callAsync promise三个触发钩子的函数有所不同,他们和对应的_call _callAsync _promise分别指向CALL_DELEGATE CALL_ASYNC_DELEGATE PROMISE_DELEGATE

const CALL_DELEGATE = function(...args) {
	this.call = this._createCall("sync");
	return this.call(...args);
};
const CALL_ASYNC_DELEGATE = function(...args) {
	this.callAsync = this._createCall("async");
	return this.callAsync(...args);
};
const PROMISE_DELEGATE = function(...args) {
	this.promise = this._createCall("promise");
	return this.promise(...args);
};

当我们调用call时,实际调用CALL_DELEGATE,此时会先通过this._createCall("sync")生成一个新的call函数,并且执行返回这个新的call函数,当我们再次执行call则直接执行,不会通过CALL_DELEGATE,可以看出这是一种懒编译。

_createCall是什么

_createCall(type) {
	return this.compile({
		taps: this.taps,
		interceptors: this.interceptors,
		args: this._args,
		type: type
	});
}

很简单,其实就是调用this.compile,而compile又是抽象函数,要由子类实现,当然就是十个不同的Hook,具体实现在之后小节。

再看一个函数_resetCompilation

_resetCompilation() {
	this.call = this._call;
	this.callAsync = this._callAsync;
	this.promise = this._promise;
}

这个函数会将我们的call callAsync promise重新指向_call _callAsync _promise,其实也就是CALL_DELEGATE CALL_ASYNC_DELEGATE PROMISE_DELEGATE,也就是重置编译。在intercept_insert函数中都会调用。

结论

  1. Hook的三种触发方式都是懒编译,在第一次触发hook时编译触发函数。
  2. 三种tap只是不同的options.type,内部_tap流程相同。
  3. 拦截器interceptor.register会在tap新的钩子时触发,intercept(interceptor)新添加的拦截器也会立即触发。
  4. tap新的钩子和intercept新的拦截器会重置触发函数的编译。

其他Hook

Sync*Hook

class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({...});
	}
}
const factory = new SyncHookCodeFactory();

const TAP_ASYNC = () => { throw new Error("tapAsync is not supported on a SyncHook"); };
const TAP_PROMISE = () => { throw new Error("tapPromise is not supported on a SyncHook"); };
const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};

function SyncHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	hook.constructor = SyncHook;
	hook.tapAsync = TAP_ASYNC;
	hook.tapPromise = TAP_PROMISE;
	hook.compile = COMPILE;
	return hook;
}
SyncHook.prototype = null;
module.exports = SyncHook;

每个不同的Sync*Hook构造函数类同,只是Sync*HookCodeFactory不同,构造函数我们可以看到,tapAsync, tapPromise只想了两个报错函数,很简单,同步钩子不支持这两种方式。

然后我们看到了Hook中缺少的compile函数,compile又调用了工厂函数。工厂等下再说,先看看Async*Hook长什么样子。

Async*Hook

class AsyncSeriesHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone }) {
		return this.callTapsSeries({...});
	}
}
const factory = new AsyncSeriesHookCodeFactory();

const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};
function AsyncSeriesHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	hook.constructor = AsyncSeriesHook;
	hook.compile = COMPILE;
	hook._call = undefined;
	hook.call = undefined;
	return hook;
}
AsyncSeriesHook.prototype = null;
module.exports = AsyncSeriesHook;

你会发现Async*Hook的结构差不多,只是没有了call

结论

  1. Sync*HookAsync*Hook的不同之处在于同步函数只能通过tap绑定钩子,Async*Hook不能通过call触发钩子。
  2. 好像没这么简单,compile都指向了工厂函数,工厂函数里边有啥?

HookCodeFactory

class HookCodeFactory {
	constructor(config) {
		this.config = config;
		this.options = undefined;
		this._args = undefined;
	}

	create(options) {
		this.init(options);
		switch (this.options.type) {
			case "sync":
		        // 省略
				break;
			case "async":
		        // 省略
				break;
			case "promise":
		        // 省略
				break;
		}
		fn = new Function(args, fnBody);
		this.deinit();
		return fn;
	}

	setup(instance, options) {
		instance._x = options.taps.map(t => t.fn);
	}

	init(options) {
		this.options = options;
		this._args = options.args.slice();
	}

	deinit() {
		this.options = undefined;
		this._args = undefined;
	}

	contentWithInterceptors(options) {
    	// reture code
	}
	header() {
		let code = "";
        // 省略
		return code;
	}
	needContext() {
		for (const tap of this.options.taps) if (tap.context) return true;
		return false;
	}
	callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
		let code = "";
        // 省略
		return code;
	}
	callTapsSeries({...}) {
		let code = "";
        // 省略
		return code;
	}
	callTapsLooping({ onError, onDone, rethrowIfPossible }) {
		let code = "";
        // 省略
		return code;
	}
	callTapsParallel({...}) {
		let code = "";
        // 省略
		return code;
	}

	args({ before, after } = {}) {
    	// 返回 [before, this._args, after].join(', ')
        // 当然要判断before, after是否存在
	}
	getTapFn(idx) {	return `_x[${idx}]`; }
	getTap(idx) { return `_taps[${idx}]`; }
	getInterceptor(idx) { return `_interceptors[${idx}]`; }
}

module.exports = HookCodeFactory;

省略了大部分代码,简单说,HookCodeFactory生成不同的执行函数,这也是每个Hook所不同的地方,setup就是将Hook.taps挂在Hook._x,重点在create,根据不同调用方式callcallAsync, promise生成不同的执行代码。

create()

create前后分别initdeinit,这个很简单就是将options挂在工厂实例,编译后清空,方便中间过程调用。

具体生成代码太长就不放了,可以告诉大家里边调用了this.args(), this.header(), this.contentWithInterceptors(),所以将目光转向这三个函数。

args(), header()

这两个函数不是很复杂,一个是生成参数列表,一个是用来触发拦截器interceptor.call()

contentWithInterceptors()

这个地方比较重要了,主要代码都是在这里生成。

contentWithInterceptors(options) {
	if (this.options.interceptors.length > 0) {
		const onError = options.onError;
		const onResult = options.onResult;
		const onDone = options.onDone;
		return this.content(
			Object.assign(options, {
				onError:
					onError &&
					(err => {
						let code = "";
						for (let i = 0; i < this.options.interceptors.length; i++) {
							const interceptor = this.options.interceptors[i];
							if (interceptor.error) {
								code += `${this.getInterceptor(i)}.error(${err});\n`;
							}
						}
						code += onError(err);
						return code;
					}),
				onResult:
					onResult &&
					(result => {
						let code = "";
						for (let i = 0; i < this.options.interceptors.length; i++) {
							const interceptor = this.options.interceptors[i];
							if (interceptor.result) {
								code += `${this.getInterceptor(i)}.result(${result});\n`;
							}
						}
						code += onResult(result);
						return code;
					}),
				onDone:
					onDone &&
					(() => {
						let code = "";
						for (let i = 0; i < this.options.interceptors.length; i++) {
							const interceptor = this.options.interceptors[i];
							if (interceptor.done) {
								code += `${this.getInterceptor(i)}.done();\n`;
							}
						}
						code += onDone();
						return code;
					})
			})
		);
	} else {
		return this.content(options);
	}
}

这里主要处理了onError onResult onDone,分别触发拦截器的error result done,此处又出现一个函数this.content,你会发现代码里并没有这个函数,因为他是在每个不同的钩子对应的源码里分别实现的。不同的钩子又会用到HookCodeFactorycallTapsSeries callTapsLooping callTapsParallel,分别对应串行,循环,并行不同的执行流程。源码都是用构造器生成的函数,可读性差,这里直接贴出来编译好的函数代码。

编译后代码

SyncHook

函数call()

"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(arg1);
var _fn1 = _x[1];
_fn1(arg1);

直接依次调用函数

函数callAsync()

"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _hasError0 = false;
try {
	_fn0(arg1);
} catch (_err) {
	_hasError0 = true;
	_callback(_err);
}
if (!_hasError0) {
	var _fn1 = _x[1];
	var _hasError1 = false;
	try {
		_fn1(arg1);
	} catch (_err) {
		_hasError1 = true;
		_callback(_err);
	}
	if (!_hasError1) {
		_callback();
	}
}

可以看出函数的执行过程是一样的,只是结果用callback形式,没有返回值,只有报错。

函数promise()

"use strict";
return new Promise((_resolve, _reject) => {
	var _sync = true;
	function _error(_err) {
		if (_sync)
			_resolve(
				Promise.resolve().then(() => {
					throw _err;
				})
			);
		else _reject(_err);
	}
	var _context;
	var _x = this._x;
	var _fn0 = _x[0];
	var _hasError0 = false;
	try {
		_fn0(arg1);
	} catch (_err) {
		_hasError0 = true;
		_error(_err);
	}
	if (!_hasError0) {
		var _fn1 = _x[1];
		var _hasError1 = false;
		try {
			_fn1(arg1);
		} catch (_err) {
			_hasError1 = true;
			_error(_err);
		}
		if (!_hasError1) {
			_resolve();
		}
	}
	_sync = false;
});

同样的执行过程,只是结果是Promise

结论

  1. 三种调用方法不影响函数内部执行过程,只是返回的结果和形式不同,同步函数也能callAsyncpromise触发钩子,只是不能挂载异步钩子处理函数。
  2. 三种不同的tap方法对应不同类型的钩子处理函数。

SyncBailHook

函数call()

"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(arg1);
if (_result0 !== undefined) {
	return _result0;
} else {
	var _fn1 = _x[1];
	var _result1 = _fn1(arg1);
	if (_result1 !== undefined) {
		return _result1;
	} else {
	}
}

可以看出函数只要有返回值就直接返回该值。 callAsyncpromise同样,内部执行逻辑相同,只是通过callbackpromise返回结果。

结论

  1. *BailHook对应的钩子只要有返回值,则整个执行过程立即结束并返回该值。

SyncLoopHook

函数callAsync()

"use strict";
var _context;
var _x = this._x;
var _loop;
do {
	_loop = false;
	var _fn0 = _x[0];
	var _hasError0 = false;
	try {
		var _result0 = _fn0(arg1);
	} catch (_err) {
		_hasError0 = true;
		_callback(_err);
	}
	if (!_hasError0) {
		if (_result0 !== undefined) {
			_loop = true;
		} else {
			var _fn1 = _x[1];
			var _hasError1 = false;
			try {
				var _result1 = _fn1(arg1);
			} catch (_err) {
				_hasError1 = true;
				_callback(_err);
			}
			if (!_hasError1) {
				if (_result1 !== undefined) {
					_loop = true;
				} else {
					if (!_loop) {
						_callback();
					}
				}
			}
		}
	}
} while (_loop);

结论

直接结论

  1. *LoopHook会循环执行钩子函数,一旦报错或者返回空值则结束循环。

SyncWaterfallHook

函数promise()

"use strict";
return new Promise((_resolve, _reject) => {
	var _sync = true;
	function _error(_err) {
		if (_sync)
			_resolve(
				Promise.resolve().then(() => {
					throw _err;
				})
			);
		else _reject(_err);
	}
	var _context;
	var _x = this._x;
	var _fn0 = _x[0];
	var _hasError0 = false;
	try {
		var _result0 = _fn0(arg1);
	} catch (_err) {
		_hasError0 = true;
		_error(_err);
	}
	if (!_hasError0) {
		if (_result0 !== undefined) {
			arg1 = _result0;
		}
		var _fn1 = _x[1];
		var _hasError1 = false;
		try {
			var _result1 = _fn1(arg1);
		} catch (_err) {
			_hasError1 = true;
			_error(_err);
		}
		if (!_hasError1) {
			if (_result1 !== undefined) {
				arg1 = _result1;
			}
			_resolve(arg1);
		}
	}
	_sync = false;
});

结论

  1. *WaterfallHook同普通Hook区别在于会将前一个hook函数执行的结果作为下一个hook函数的参数。

AsyncSeriesHook

同上所得结论,异步hook没有call函数,但是有三种tap函数。 以下代码是分别调用三种tap函数挂载了三个不同处理函数。

函数callAsync()

"use strict";
var _context;
var _x = this._x;
function _next1() {
	var _fn2 = _x[2];
	var _hasResult2 = false;
	var _promise2 = _fn2(arg1);
	if (!_promise2 || !_promise2.then)
		throw new Error(
			"Tap function (tapPromise) did not return promise (returned " +
				_promise2 +
				")"
		);
	_promise2.then(
		_result2 => {
			_hasResult2 = true;
			_callback();
		},
		_err2 => {
			if (_hasResult2) throw _err2;
			_callback(_err2);
		}
	);
}
var _fn0 = _x[0];
var _hasError0 = false;
try {
	_fn0(arg1);
} catch (_err) {
	_hasError0 = true;
	_callback(_err);
}
if (!_hasError0) {
	var _fn1 = _x[1];
	_fn1(arg1, _err1 => {
		if (_err1) {
			_callback(_err1);
		} else {
			_next1();
		}
	});
}

可以看出,第一个tap挂载的函数是普通的调用方式,第二个tapAsync的函数是异步调用方式,第三个tapPromise的函数是要返回promise

结论

  1. tap挂载普通函数。
  2. tapAsync挂载的函数需要有callback形参,也就是
hook.tapAsync('name', (args, callback) => {
    callback(error)
})
  1. tapPromise挂载的函数需要返回promise,也就是
hook.tapAsync('name', (args) => {
    return Promise.resolve(result)
})
  1. 所以现在应该能明白三种tap函数为什么要这么用。

AsyncSeriesBailHook, AsyncSeriesLoopHook, AsyncSeriesWaterfallHook

略,和同步函数类似,三种分别是:

  1. 钩子处理函数有返回值则返回结果。
  2. 循环执行,没有返回值或报错则结束循环。
  3. 钩子处理函数的返回结果会作为下一个处理函数的参数。

AsyncParallelHook

函数callAsync()

var _context;
var _x = this._x;
do {
	var _counter = 4;
	var _done = () => {
		_callback();
	};
	if (_counter <= 0) break;
	var _fn0 = _x[0];
	var _hasError0 = false;
	try {
		_fn0(arg1);
	} catch (_err) {
		_hasError0 = true;
		if (_counter > 0) {
			_callback(_err);
			_counter = 0;
		}
	}
	if (!_hasError0) {
		if (--_counter === 0) _done();
	}
	if (_counter <= 0) break;
	var _fn1 = _x[1];
	var _hasError1 = false;
	try {
		_fn1(arg1);
	} catch (_err) {
		_hasError1 = true;
		if (_counter > 0) {
			_callback(_err);
			_counter = 0;
		}
	}
	if (!_hasError1) {
		if (--_counter === 0) _done();
	}
	if (_counter <= 0) break;
	var _fn2 = _x[2];
	_fn2(arg1, _err2 => {
		if (_err2) {
			if (_counter > 0) {
				_callback(_err2);
				_counter = 0;
			}
		} else {
			if (--_counter === 0) _done();
		}
	});
	if (_counter <= 0) break;
	var _fn3 = _x[3];
	var _hasResult3 = false;
	var _promise3 = _fn3(arg1);
	if (!_promise3 || !_promise3.then)
		throw new Error(
			"Tap function (tapPromise) did not return promise (returned " +
				_promise3 +
				")"
		);
	_promise3.then(
		_result3 => {
			_hasResult3 = true;
			if (--_counter === 0) _done();
		},
		_err3 => {
			if (_hasResult3) throw _err3;
			if (_counter > 0) {
				_callback(_err3);
				_counter = 0;
			}
		}
	);
} while (false);

同时执行全部函数,用count记录函数数量,每执行完毕一个count--count===0时执行回调。返回错误信息则count=0直接结束。

结论

  1. AsyncParallelHook异步并行钩子会直接执行全部函数,不会等待函数执行结束后执行下一个,当然如果tap()同步处理函数,并行不并行没有区别了。

AsyncParallelBailHook

函数promise()

"use strict";
return new Promise((_resolve, _reject) => {
	var _sync = true;
	function _error(_err) {
		if (_sync)
			_resolve(
				Promise.resolve().then(() => {
					throw _err;
				})
			);
		else _reject(_err);
	}
	var _context;
	var _x = this._x;
	var _results = new Array(4);
	var _checkDone = () => {
		for (var i = 0; i < _results.length; i++) {
			var item = _results[i];
			if (item === undefined) return false;
			if (item.result !== undefined) {
				_resolve(item.result);
				return true;
			}
			if (item.error) {
				_error(item.error);
				return true;
			}
		}
		return false;
	};
	do {
		var _counter = 4;
		var _done = () => {
			_resolve();
		};
		if (_counter <= 0) break;
		var _fn0 = _x[0];
		var _hasError0 = false;
		try {
			var _result0 = _fn0(arg1);
		} catch (_err) {
			_hasError0 = true;
			if (_counter > 0) {
				if (
					0 < _results.length &&
					((_results.length = 1), (_results[0] = { error: _err }), _checkDone())
				) {
					_counter = 0;
				} else {
					if (--_counter === 0) _done();
				}
			}
		}
		if (!_hasError0) {
			if (_counter > 0) {
				if (
					0 < _results.length &&
					(_result0 !== undefined && (_results.length = 1),
					(_results[0] = { result: _result0 }),
					_checkDone())
				) {
					_counter = 0;
				} else {
					if (--_counter === 0) _done();
				}
			}
		}
		if (_counter <= 0) break;
		if (1 >= _results.length) {
			if (--_counter === 0) _done();
		} else {
			var _fn1 = _x[1];
			var _hasError1 = false;
			try {
				var _result1 = _fn1(arg1);
			} catch (_err) {
				_hasError1 = true;
				if (_counter > 0) {
					if (
						1 < _results.length &&
						((_results.length = 2),
						(_results[1] = { error: _err }),
						_checkDone())
					) {
						_counter = 0;
					} else {
						if (--_counter === 0) _done();
					}
				}
			}
			if (!_hasError1) {
				if (_counter > 0) {
					if (
						1 < _results.length &&
						(_result1 !== undefined && (_results.length = 2),
						(_results[1] = { result: _result1 }),
						_checkDone())
					) {
						_counter = 0;
					} else {
						if (--_counter === 0) _done();
					}
				}
			}
		}
		if (_counter <= 0) break;
		if (2 >= _results.length) {
			if (--_counter === 0) _done();
		} else {
			var _fn2 = _x[2];
			_fn2(arg1, (_err2, _result2) => {
				if (_err2) {
					if (_counter > 0) {
						if (
							2 < _results.length &&
							((_results.length = 3),
							(_results[2] = { error: _err2 }),
							_checkDone())
						) {
							_counter = 0;
						} else {
							if (--_counter === 0) _done();
						}
					}
				} else {
					if (_counter > 0) {
						if (
							2 < _results.length &&
							(_result2 !== undefined && (_results.length = 3),
							(_results[2] = { result: _result2 }),
							_checkDone())
						) {
							_counter = 0;
						} else {
							if (--_counter === 0) _done();
						}
					}
				}
			});
		}
		if (_counter <= 0) break;
		if (3 >= _results.length) {
			if (--_counter === 0) _done();
		} else {
			var _fn3 = _x[3];
			var _hasResult3 = false;
			var _promise3 = _fn3(arg1);
			if (!_promise3 || !_promise3.then)
				throw new Error(
					"Tap function (tapPromise) did not return promise (returned " +
						_promise3 +
						")"
				);
			_promise3.then(
				_result3 => {
					_hasResult3 = true;
					if (_counter > 0) {
						if (
							3 < _results.length &&
							(_result3 !== undefined && (_results.length = 4),
							(_results[3] = { result: _result3 }),
							_checkDone())
						) {
							_counter = 0;
						} else {
							if (--_counter === 0) _done();
						}
					}
				},
				_err3 => {
					if (_hasResult3) throw _err3;
					if (_counter > 0) {
						if (
							3 < _results.length &&
							((_results.length = 4),
							(_results[3] = { error: _err3 }),
							_checkDone())
						) {
							_counter = 0;
						} else {
							if (--_counter === 0) _done();
						}
					}
				}
			);
		}
	} while (false);
	_sync = false;
});

用数组存储每个函数的结果,在函数执行结束时count--并判定是否存在结果,有的话就结束。

结论

  1. 并行熔断机制,一旦有函数返回结果就结束。