用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,
}
相关推荐
m0_5485147716 分钟前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
AndrewPerfect16 分钟前
xss csrf怎么预防?
前端·xss·csrf
Calm55019 分钟前
Vue3:uv-upload图片上传
前端·vue.js
浮游本尊24 分钟前
Nginx配置:如何在一个域名下运行两个网站
前端·javascript
m0_7482398324 分钟前
前端bug调试
前端·bug
m0_7482329227 分钟前
[项目][boost搜索引擎#4] cpp-httplib使用 log.hpp 前端 测试及总结
前端·搜索引擎
新中地GIS开发老师32 分钟前
《Vue进阶教程》(12)ref的实现详细教程
前端·javascript·vue.js·arcgis·前端框架·地理信息科学·地信
m0_7482495434 分钟前
前端:base64的作用
前端
html组态40 分钟前
web组态可视化编辑器
前端·物联网·编辑器·web组态·组态·组态软件
~央千澈~1 小时前
如果你的网站是h5网站,如何将h5网站变成小程序-除开完整重做方法如何快速h5转小程序-h5网站转小程序的办法-优雅草央千澈
前端·apache