盐焗乳鸽还要香锅

V1

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 :异步执行,支持传入 callbackpromise 风格的异步回调函数,支持 callAsync/tapAsyncpromise/tapPromise 两种调用语句。

这里callAsyncpromise的区别在于:

  • 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

SyncBoilHookSyncHook最大的区别就是在「回调函数有返回值的时候,会停止执行后续的回调函数」。源码实现的细节就是在于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

SyncWaterfallHookSyncHook最大的区别在于「回调函数的返回值会传递给下一个回调函数」。因此,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

分类:

前端

标签:

前端

作者介绍

盐焗乳鸽还要香锅
V1