总要有个前言
想必看这篇文章的各位总知道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.stage和options.before插入到this.taps对应的位置。
- 标准化
options - 触发拦截器
register _insert进this.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函数中都会调用。
结论
- Hook的三种触发方式都是懒编译,在第一次触发hook时编译触发函数。
- 三种
tap只是不同的options.type,内部_tap流程相同。 - 拦截器
interceptor.register会在tap新的钩子时触发,intercept(interceptor)新添加的拦截器也会立即触发。 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。
结论
Sync*Hook和Async*Hook的不同之处在于同步函数只能通过tap绑定钩子,Async*Hook不能通过call触发钩子。- 好像没这么简单,
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,根据不同调用方式call,callAsync, promise生成不同的执行代码。
create()
create前后分别init和deinit,这个很简单就是将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,你会发现代码里并没有这个函数,因为他是在每个不同的钩子对应的源码里分别实现的。不同的钩子又会用到HookCodeFactory的 callTapsSeries 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
结论
- 三种调用方法不影响函数内部执行过程,只是返回的结果和形式不同,同步函数也能
callAsync和promise触发钩子,只是不能挂载异步钩子处理函数。 - 三种不同的
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 {
}
}
可以看出函数只要有返回值就直接返回该值。
callAsync和promise同样,内部执行逻辑相同,只是通过callback和promise返回结果。
结论
*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);
结论
直接结论
*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;
});
结论
*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。
结论
tap挂载普通函数。tapAsync挂载的函数需要有callback形参,也就是
hook.tapAsync('name', (args, callback) => {
callback(error)
})
tapPromise挂载的函数需要返回promise,也就是
hook.tapAsync('name', (args) => {
return Promise.resolve(result)
})
- 所以现在应该能明白三种
tap函数为什么要这么用。
AsyncSeriesBailHook, AsyncSeriesLoopHook, AsyncSeriesWaterfallHook
略,和同步函数类似,三种分别是:
- 钩子处理函数有返回值则返回结果。
- 循环执行,没有返回值或报错则结束循环。
- 钩子处理函数的返回结果会作为下一个处理函数的参数。
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直接结束。
结论
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--并判定是否存在结果,有的话就结束。
结论
- 并行熔断机制,一旦有函数返回结果就结束。