Tapable是一个专注于事件处理的模块,类似于 Nodejs 的EventEmitter
。相比EventEmitter
,Tapable 提供了例如瀑布流、线性异步、并行异步等更加高级的事件流处理机制。正如其名,通过它我们能触摸到 Webpack 编译的各个阶段,这也是 Webpack 插件机制的核心设计理念。
Hooks
Tapable 导出了多个功能各异的钩子类(Hook Class),Webpack 在编译的每个阶段都会有一个钩子实例。插件可以通过tap
,tapAync
,tapPromise
方法来注册同步或者异步回调,每个方法都有插件名称和回调函数两个参数。
让我们通过“小明的一天”这个例子来了解这些钩子吧。
同步 Hooks
每一个钩子类的构造函数都接受一个数组作为参数,这个数组决定了这个钩子触发时可以传递的参数,而这些参数会传递给每一个回调函数。
先让我们了解一下简单的同步钩子。
同步 Hooks 的最后一个回调的返回值会成为 call 方法的返回值
SyncHook
同步钩子。
1 | constructor() { |
SyncBailHook
同步钩子,如果某个回调返回了值,后续的回调会不再执行。
1 | constructor() { |
SyncWaterfallHook
同步瀑布流钩子,这个和同步钩子的区别在于,下一个回调会接受上一个返回的结果,有点像 Array 的reduce
方法。
SyncWaterfallHook
的构造函数的参数数组必须有一个值。
“我们假设小明去上学了,路上遇到了几个同学,然后记录整个过程。”
1 | constructor() { |
SyncLoopHook
吐槽一下官方文档,只留了个
TODO
,关于这个的文档和 Demo 都没有,只能通过搜索资料和看源码分析怎么用。
当监听函数被触发的时候,如果该监听函数返回 true 时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环。
1 | let queue = new SyncLoopHook([']); |
异步 Hooks
现在很多任务都是异步操作,所以 tapable 还提供了几种异步钩子。
而异步的情况比同步就要复杂一些,异步可以并行(Parallel)执行和线性(Series)执行,同时也会有 Bail, Waterfall 等类型。
下面是不同执行态的 Hook 支持的类型。
执行\类型 | Basic | Bail | Waterfall |
---|---|---|---|
Sync | ✅ | ✅ | ✅ |
AsyncSeries | ✅ | ✅ | ✅ |
AsyncParallel | ✅ | ✅ | ❌ |
异步 hooks 有额外的tapAsync
,tapPromise
两种方式来注册异步回调,也就是支持 callback 参数和 promise 的方式来异步返回结果。
因为是并行执行,所以异步并行钩子并没有瀑布类型。
AsyncSeriesHook
异步线性钩子,和同步钩子很相似,区别在于回调函数内部可以是异步的,并在异步完成后通知钩子执行下一个回调。
“我们继续小明的一天,现在老师来了,小明作为小组长开始收集组内同学昨天的作业。”
1 | constructor() { |
需要注意的是异步钩子的最后一个回调函数的返回值并不会成为触发方法的返回值,所以在整个调用链是直接对homeworks
对象进行操作,而不是返回新的对象。
AsyncSeriesBailHook
与 SyncBailHook 类似的,只要某个异步回调返回了值,那么会中断调用链,剩下的回调将会执行。
“今天班级组织了一次抽奖活动,奖品只有一个,谁先抽到谁得。”
1 | constructor() { |
因为小张已经中奖了,所以小明也没有机会再去抽奖了。
AsyncSeriesWaterfallHook
线性异步瀑布流钩子,和 SyncWaterfallHook
的区别在于把同步的回调改成了异步。
抽奖活动后,班级组织了“成语接龙”的游戏。
1 | constructor() { |
AsyncParallelHook
前面三个异步钩子都是线性的,现在让我们来了解异步并行钩子。顾名思义,注册的回调是并行执行的。
“娱乐过后,老师突然宣布进行单元测验。”
1 | constructor() { |
AsyncParallelBailHook
异步并发,对回调函数的返回值进行依次检测,只要有一个回调函数返回了值,就取其值作为结果。和 SyncBailHook 不同在于,后者是同步执行,所以剩下的回调函数不会执行,而这里是异步的并行执行,所以回调函数本身仍然是执行过的。
AsyncParallelBailHook 对返回值的检测仍然是按回调执行的顺序来的,如果第 1 个回调的处理时间比第 2 个长,那么仍然会等待第一个完成后才会触发异步 call 的回调。如果第 1 个回调也有返回值,即使第 2 个响应更快且有返回值,还是会以第一个为准。
1 | constructor() { |
拦截器(Interception)
所有的 Hooks 都可以通过intercept
接口添加拦截器。拦截器有几个触发时机。
- call:(…args) => void - 当 hook 触发时执行,参数是 hook 传递的参数。
- tap:(tap:Tap) => void - 当 hook 注册回调时执行。
Tap
是回调的信息。 - loop:(…args) => void - 当 loop hook 的每一次循环触发时执行。
- register:(tap:Tap) => void - 当触发每一个回调时执行。
1 | interface Tap { |
Context(上下文)
插件和拦截器可以访问一个可选的context
上下文对象,可以在回调和拦截器之间传递数据。
只要声明context: true
,就会多一个 context 参数。
1 | xiaoming.hooks.wakeUp.tap({ name: "Plugin", context: true }, context => { |
结语
后续将研究 Tapable 的源码以及在 Webpack 中的实际应用。
完整DEMO