tapable 的钩子函数根据事件运行机制来分可以划分成四种:串行机制、中断机制、流水线机制和循环机制;根据运行模式可以划分成同步和异步,同时异步又可分为异步串行和异步并行。
只有串行情况下,才能实现流水线机制,所以异步并行没有流水线机制的钩子。同时并行的情况下其实循环机制也难以控制,所有异步并行也没有循环机制的钩子。
同步钩子(同步串行)
- SyncHook(串行)
- SyncBailHook(中断)
- SyncWaterfallHook(流水线)
- SyncLoopHook(循环)
异步钩子
异步分为异步串行和异步并行两种。
异步串行
- AsyncSeriesHook(串行)
- AsyncSeriesBailHook(中断)
- AsyncSeriesWaterfallHook(流水线)
- AsyncSeriesLoopHook(循环)
异步并行
- AsyncParallelHook
- AsyncParallelBailHook(中断)
基本使用(以 SyncHook 为例)
需要注意的有以下两点:
- 在实例化钩子的时候就限制了参数数量,后面即使调用方(hook.call)和接收方(hook.tap)约定好参数数量,hook.tap 实际上也不会收到多出的参数。
- hook.tap 可以注册多个事件,多个事件会按注册顺序执行,hook.call 可以调用多次。
同步钩子
同步钩子使用同步的订阅方式 hook.tap
以及同步的触发方式 hook.call
。
SyncHook
这个钩子最简单,它只是按照事件的注册顺序依次调用,所注册的事件之间相互独立,没有任何关联。
SyncBailHook
这个钩子与 SyncHook 类似,也是顺序调用注册事件。但不同的是:它多了一种中断机制,也就是当前一个事件返回值 不是 undefined 时,会中断后面事件的执行。
SyncWaterfallHook
这个钩子与 SyncHook 类似,也是顺序调用注册事件。但不同的是,它加入了流水线设计,像流水一样,上流会影响下流(也就是上一个事件的 return 会作为下一个事件的参数)。
上一个事件的返回值会覆盖第一个参数。
SyncLoopHook
这个钩子相对是最复杂的一个钩子,它设计了一种循环规则,实现注册事件的循环调用。那么这种循环规则就是:先顺序执行注册事件,当事件返回值不是 undefined 时,再从头开始执行注册事件,直到所有事件都返回 undefined 时,循环结束。
异步钩子
异步钩子采用异步的订阅方式和异步的触发方式,而我们知道 js 异步编程总共有两种解决方案:callback 和 promise。所有 tapable 也提供这两种方式的订阅钩子:hook.tapAsync
、hook.tapPromise
,以及触发方式:hook.callAsync
、hook.promise
。
异步串行
AsyncSeriesHook
剩余三个与同步的相同机制类似,这里不再赘述。
异步并行
AsyncParallelBailHook
异步并行我们只需要看一眼 AsyncParallelBailHook 这个钩子即可,它是只要有一个事件达到中断条件,整体结果就完成了。
可以看到当 done2 结束时,return 了一个 undefined,所以并没有达到中断条件。过了 1s 后,done1 结束 return 了一个非 undefined,达到中断条件,于是中断完成,也就是 done 输出在 [ 'a' ] done3
之前。
简单实现
了解完了 tapable 有这十种钩子,我们可以简单来实现一下。
同步串行
简单分析一下,很显然事件的订阅方法和钩子的触发方法是相同的,这一块我们可以提取到一个公共类中 Hooks
。如下:
而钩子触发之后具体的事件调用逻辑是各不相同的,所以 execute
方法可以在各个子类中去实现。代码如下:
异步串行
实现了同步串行的四个钩子之后,再实现异步串行的四个钩子其实就十分简单了,只需要 await 一下即可。我们先在基础 Hooks 上增加两个订阅事件和触发事件方法,如下:
call 方法可以复用,需要改造一下,因为异步钩子需要多接收一个 callback 参数。但是订阅的时候,我们需要记录一下订阅的类型,因为 callback 和 promise 的等待方式不一样。
其他的流程是一样的,只需要注意 await,代码如下:
异步并发
最后我们再实现两个异步并发的钩子。对于 AsyncParallelHook 我们只需要使用 Promise.all 装一下即可,代码如下:
对于 AsyncParallelBailHook 钩子,只要出现有一个结果不为 undefined,那么就结束,所以我们使用 Promise.any(这个方法会等到第一个成功才结束,利用这种特性,当返回值不为 undefined 的时候,我们可以手动构造一个 reject) 包一下。代码如下:
其实到目前位置 tapable 的所有钩子就基本实现了,当然这是十分简易的版本,还有一些错误处理和细枝末节,我们就不处理了。
小疑问
如果看过 tapable 源码的朋友可能知道,我们如下的源代码:
会被编译成如下代码,再进去运行:
那么为什么 tapable 要这样做呢?
有兴趣的朋友可以看看这个 issues:Is the new Function performance really good?。我大概看了一下,大概意思是:当数量比较大时,浏览器会进行优化,使用 new Function 的方式会占一点点优势。正如下图:
附录(完整代码)
原文链接:https://juejin.cn/post/7354940462061207593