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