盐焗乳鸽还要香锅
2022/11/26阅读:32主题:默认主题
源码系列之Tapable--第二弹
源码系列之 Tapable--第二弹
经过了源码系列之 Tapable--第一弹对SyncHook
的了解,我们接下来来看Tapable
的更多的架构功能,首先来看看官网的用法简介:
用法
首先 Hook 的执行方式取决于钩子的类型:
-
基本 Hook (without “Waterfall”, “Bail” or “Loop” in its name). 它会简单地顺序执行注册的回调函数,不会处理回调函数的返回值。
-
Waterfall. 瀑布 Hook 也会顺序执行注册的回调函数。. 不像基本 Hook, 它会处理返回值并且传递给下一个回调函数.
-
Boil. 熔断 Hook 当有一个回调函数有返回值的时候,会立即停止执行剩下的回调函数。
-
Loop. 当循环钩子中的插件返回非 undefined 值时,钩子将从第一个插件重新启动。它将循环,直到所有插件返回未定义。
Hook 也有三种类型:
-
Sync. 只能使用
hook.tap()
进行注册。 -
AsyncSeries. 可以使用
myHook.tap()
、myHook.tapAsync()
、myHook.tapPromise()
. They call each async method in a row. -
AsyncParallel. An async-parallel hook can also be tapped with synchronous, callback-based and promise-based functions (using myHook.tap(), myHook.tapAsync() and myHook.tapPromise()). However, they run each async method in parallel.
按执行回调的并行方式,分为:
-
sync :同步执行,启动后会按次序逐个执行回调,支持
call/tap
调用语句; -
async :异步执行,支持传入
callback
或promise
风格的异步回调函数,支持callAsync/tapAsync
、promise/tapPromise
两种调用语句。
这里
callAsync
与promise
的区别在于:
callAsync
只是多了一个可以用于处理错误的回调函数promise
返回值是一个 Promise 对象
除此之外,还支持「拦截器」功能:
-
call: (...args) => void
当 Hook「触发」时会被调用,你还可以获取 Hook 的参数。 -
tap: (tap: Tap) => void
当 Hook「触发」时会被调用,可以获取 tap 对象,但不可以改变。 -
loop: (...args) => void
当每一次 looping hook 发生循环时都会被触发 -
register: (tap: Tap) => Tap | undefined
在回调函数注册时触发,可以获取到每一个添加的 Tap 对象,且可以改变它们。
举个例子:
SyncHook/callAsync/tap
需要传入一个回调函数callAsync(cb:(_err)=>void)
,用于处理可能发生的错误,如果有错则停止执行,并且将错误作为参数传给回调函数。如果没有错误,则在最后传递一个 null,并执行回调函数:
hook.callAsync((err) => {
console.log(err);
});
// 最终生成的回调函数
(function anonymous(_callback
) {
// 如果有设置interceptor.call的话
var _interceptors = this.interceptors;
_interceptors[0].call(参数);
// 如果设置了interceptor.tap的话
var _tap0 = _taps[0]; // {type,name,fn}
_interceptors[0].tap(_tap0);
...
var _fn0 = _x[0];
var _hasError0 = false;
try {
_fn0();
} catch(_err) {
_hasError0 = true;
_callback(_err);
}
if(!_hasError0) {
var _fn1 = _x[1];
var _hasError1 = false;
try {
_fn1();
} catch(_err) {
_hasError1 = true;
_callback(_err);
}
...
_callback(null)
})
SyncBoilHook
SyncBoilHook
与SyncHook
最大的区别就是在「回调函数有返回值的时候,会停止执行后续的回调函数」。源码实现的细节就是在于content()
函数的实现,如果result !== undefined
的话,就直接return
出去。
class SyncBailHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, resultReturns, 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`,
resultReturns,
onDone,
rethrowIfPossible
});
}
}
SyncWaterfallHook
SyncWaterfallHook
与SyncHook
最大的区别在于「回调函数的返回值会传递给下一个回调函数」。因此,SyncWaterfallHook
在初始化时args
的长度必须大于 0,在使用call
执行 hook 时也要传入参数!
从下面源码我们可以看到,当上一个返回值result
不是undefined
的时候,就会赋值给_args[0]
。
class SyncWaterfallHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, resultReturns, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onResult: (i, result, next) => {
let code = "";
code += `if(${result} !== undefined) {\n`;
code += `${this._args[0]} = ${result};\n`;
code += `}\n`;
code += next();
return code;
},
onDone: () => onResult(this._args[0]),
doneReturns: resultReturns,
rethrowIfPossible
});
}
}
因此,当有多个参数的时候,我们要记住它会类似reduce
方法一样,回调函数的第一个参数是前面的回调函数返回值的累积结果:
const waterfallHook = new SyncWaterfallHook(['a', 'b'])
waterfallHook.tap("test1", (res1, res2) => {
return res1 + res2 + '?'
})
waterfallHook.tap("test2", (res1, res2) => {
return res1 + res2 + '?'
})
const value = waterfallHook.call("hello", "world")
console.log(value); // helloworld?world?
SyncLoopHook
SyncLoopHook
最大的特点就是「回调函数返回值不为undefined
的时候,将会从第一个注册的插件开始从头执行,直到返回值为undefined
为止」
要注意,SyncLoopHook
并不会将返回值传递给后续的事件,其参数始终为调用执行 hook(即call(args)
)时的参数。
AsyncSeriesHook
异步系列 Hook,它是没有call
方法的。,顾名思义,是串行地去执行同步/异步函数。
我们举个例子看看它到底是怎么用的吧:
const asyncSeriesHook = new AsyncSeriesHook([]);
asyncSeriesHook.tap("c", () => {
console.log('c');
})
asyncSeriesHook.tapAsync("a", (next) => {
console.log('a');
next()
});
asyncSeriesHook.tapAsync("b", (next) => {
console.log('b');
});
asyncSeriesHook.callAsync((err) => {
console.log('call', err);
});
经过调试后发现,tapSync
注册的异步回调函数,会接受一个next()
函数的参数,需要用户去手动告诉 Hook 什么时候异步执行完毕,即调用一下 next 函数,才会执行后续的回调函数。
同时next
函数还可以接受一个错误信息,暂停执行后续的回调函数,执行在callAsync
阶段传入的错误回调函数。
AsyncSeriesBailHook
AsyncSeriesBailHook
在前面的学习中我们可以知道如果回调函数有返回值的话就会停止执行。那么结合异步函数后,我们又该如何去使用呢?
举个例子:
const asyncSeriesBailHook = new AsyncSeriesBailHook([]);
asyncSeriesBailHook.tap("c", () => {
console.log('c');
})
asyncSeriesBailHook.tapAsync("a", (next) => {
console.log('a');
// 这里的next函数注意入参
next(undefined, "a")
// 这里相当于返回值为‘a’,因此b事件不会被执行
});
asyncSeriesBailHook.tapAsync("b", (next) => {
console.log('b');
});
asyncSeriesBailHook.callAsync((err, res) => {
// 错误捕获注意入参
console.log('call', err, res);
});
注意这里的next
函数和错误捕获函数的参数发生了变化。
AsyncSeriesLoopHook
同样的,搞清楚了上述 Hook 的流程,AsyncSeriesLoopHook
的用法也可以猜的很明白了:
const asyncSeriesLoopHook = new AsyncSeriesLoopHook([]);
let time = 0
asyncSeriesLoopHook.tap("a", () => {
console.log('a');
time++
if (time > 2) {
return
}
return 'a'
})
asyncSeriesLoopHook.tapAsync("b", (next) => {
console.log('b');
next('error in b', 'b')
})
asyncSeriesLoopHook.promise().then(res => {
console.log(res);
}).catch(e => {
console.log('e', e);
})
// 打印 a a a b error in b
AsyncSeriesWaterfallHook
const asyncSeriesWaterfallHook = new AsyncSeriesWaterfallHook(['a']);
asyncSeriesWaterfallHook.tapAsync('a', (res, next) => {
next(undefined, res + '?')
})
asyncSeriesWaterfallHook.callAsync(1, (err, res) => {
console.log(err, res);
})
// null 1?
AsyncParallelHook
最后来看一下Parallel
系列的 Hook。顾名思义,这个 Hook 与上述的都不一样,注册的所有事件都是异步执行的。举个例子:
const hook = new AsyncParallelHook();
hook.tapAsync('a', (next) => {
setTimeout(() => { console.log('a'); next() }, 2000)
})
hook.tapAsync('b', (next) => {
setTimeout(() => { console.log('b'); next() }, 1000)
})
hook.tapAsync('c', (next) => {
setTimeout(() => { console.log('c'); next() }, 2000)
})
hook.tap('d', () => {
console.log('d');
})
hook.callAsync(err => {
console.log(err);
})
d
b
a
c
undefined
总结
至此,Tapable
里所有的 Hooks 都介绍完成了,其源码的实现也基本能够看懂。其中,最神秘的就是compile
函数的动态编译。它是根据new Function
来动态构建一个匿名函数。这是非常少见的实现方式。
那么我们不妨来思考一下为什么要用如此难懂晦涩的方式来实现呢?
我们拿AsyncSeriesWaterfallHook
来思考一下,我们需要在执行每一个异步函数后,还要对其返回值以及可能出现的错误进行处理,并且将返回值传递给下一个异步函数。在不用promise
的情况下,我个人认为要是用「回调地狱」代码才能实现这个功能。实际上AsyncSeriesWaterfallHook
动态编译出的匿名函数就是一段回调地狱代码。
除此之外还有HookMap
的高级用法,它可以集合不同 Hook 的操作,降低代码复杂度。在 Webpack 中也大有用处......

我是「盐焗乳鸽还要香锅」,喜欢我的文章欢迎关注噢
github 链接https://github.com/1360151219 博客链接是 strk2.cn 掘金账号https://juejin.cn/user/1812428713376845 知乎账号是https://www.zhihu.com/people/hua-qi-lun-hui
作者介绍