用200 行代码,简单写一下 tapable

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.tapAsynchook.tapPromise,以及触发方式:hook.callAsynchook.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,
}
相关推荐
掘金者阿豪38 分钟前
把业务数据变成共享仪表盘:Metabase可视化与远程访问实践
前端·后端
kyriewen1 小时前
折腾了半年 AI 编程工作流,最后发现效率瓶颈是桌上那块屏幕
前端·javascript·ai编程
蜗牛前端1 小时前
codex 全流程开发上线的高颜值礼簿小程序
前端·微信小程序
大龄秃头程序员2 小时前
我在图文流 App 里落地双层缓存、弱网降级与 OOM 治理
前端
老王以为2 小时前
React Renderer 分离的多平台架构
前端·react native·react.js
hunterandroid2 小时前
Kotlin Coroutines 与 Flow:让异步任务更清晰
前端
Bigger3 小时前
从零搭建 AI 代码审查服务:一份前端也能看懂的 Python 学习笔记
前端·ci/cd·ai编程
lichenyang4533 小时前
JSAPI、NAPI、Biz、Imp:ASCF Demo 如何真正调用系统能力和 C++ 能力
前端
lichenyang4533 小时前
IPC、JSVM、UIThread、libuv:ASCF 架构图里最容易混的几个词
前端
用户059540174463 小时前
Redis记忆存储故障恢复测试踩坑实录:手动测试让我漏掉了2个一致性Bug
前端·css