用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,
}
相关推荐
还是大剑师兰特39 分钟前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解40 分钟前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django
张张打怪兽1 小时前
css-50 Projects in 50 Days(3)
前端·css