webpack的事件机制底层——tapable

文章内容输出来源:拉勾教育前端高薪训练营

tapable是一种事件驱动型事件流机制,是一个类似于 Node.js 中的 EventEmitter的库,但更专注于自定义事件的触发和处理。webpack 通过 tapable 将实现与流程解耦,所有具体实现通过插件的形式存在。,其工作流程为
1.实例化Hook注册事件监听
2.通过Hook触发事件监听
3.执行懒编译生成的可执行代码

Hook的执行机制可分为同步和异步两种,其中异步又分为并行和串行两种模;Hook的钩子类型分为四种:

  • Hook:普通钩子,监听器之间互相独立不干扰
  • BailHook:熔断钩子,某个监听返回非undefined时后续不执行
  • WaterfallHook:瀑布钩子,上一个监听的返回值可传递至下一个
  • LoopHook:循环钩子,如果当前返回非undefined则一直执行(从头开始执行)(webpack中不常见)

同步钩子

通过tap注册钩子事件,通过call执行钩子事件

SyncHook
每个事件独立执行,互相不影响

const { SyncHook } = require('tapable')

const hook = new SyncHook(['name', 'age'])

hook.tap('fn1', (name, age) => {
  console.log(`fn1 => ${name}: ${age}`)
})

hook.tap('fn2', (name, age) => {
  console.log(`fn2 => ${name}: ${age}`)
})

hook.call('yhzzy', 18) // fn1 => yhzzy: 18  fn2 => yhzzy: 18

SyncBailHook
某个监听返回非undefined时后续不执行

const { SyncBailHook } = require('tapable')

const hook = new SyncBailHook(['name', 'age'])

hook.tap('fn1', (name, age) => {
  console.log(`fn1 => ${name}: ${age}`)
})

hook.tap('fn2', (name, age) => {
  console.log(`fn2 => ${name}: ${age}`)
  return true
})

hook.tap('fn3', (name, age) => {
  console.log(`fn3 => ${name}: ${age}`)
})

hook.call('yhzzy', 18) // fn1 => yhzzy: 18  fn2 => yhzzy: 18

SyncWaterfallHook
上一个监听的返回值可传递至下一个,覆盖原来的参数值

const { SyncWaterfallHook } = require('tapable')

const hook = new SyncWaterfallHook(['name', 'age'])

hook.tap('fn1', (name, age) => {
  console.log(`fn1 => ${name}: ${age}`)
  return 'tom'
})

hook.tap('fn2', (name, age) => {
  console.log(`fn2 => ${name}: ${age}`)
  return 'mike'
})

hook.tap('fn3', (name, age) => {
  console.log(`fn3 => ${name}: ${age}`)
  return 'jim'
})

hook.call('yhzzy', 18) // fn1 => yhzzy: 18  fn2 => tom: 18  fn3 => mike: 18

SyncLoopHook

  • 执行时,如果都没有返回,则执行一遍后就退出
  • 如果有一个返回了非undefined,则从头重新开始执行
  • 当遇到返回了undefined则执行完所有监听后退出
const { SyncLoopHook } = require('tapable')

const hook = new SyncLoopHook(['name', 'age'])

let count = 0

hook.tap('fn1', (name, age) => {
  console.log(`fn1 => ${name}: ${age}`)
})

hook.tap('fn2', (name, age) => {
  console.log(`fn2 => ${name}: ${age}`)
  if (++count === 2) {
    count = 0
    return undefined
  }
  return true
})

hook.tap('fn3', (name, age) => {
  console.log(`fn3 => ${name}: ${age}`)
})

hook.call('yhzzy', 18) // fn1 => yhzzy: 18  fn2 => yhzzy: 18 fn1 => yhzzy: 18  fn2 => yhzzy: 18  fn3 => yhzzy: 18

异步钩子

异步钩子可以使用tapPromise注册=>promise调用,或者使用tapAsync注册=>callAsync调用,每种异步钩子的作用和同步一样,只是每个监听是异步返回

异步串行钩子

异步按顺序执行

AsyncSeriesHook

const { AsyncSeriesHook } = require('tapable')

let hook = new AsyncSeriesHook(['name'])

console.time('time')
hook.tapPromise('fn1', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`fn1 => ${name}`) // fn1 => yhzzy
      resolve()
    }, 1000)
  })
})

hook.tapPromise('fn2', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`fn2 => ${name}`) // fn2 => yhzzy
      resolve()
    }, 2000)
  })
})

hook.promise('yhzzy').then(function () {
  console.timeEnd('time') // time: 3.018s
})

AsyncSeriesBailHook

const { AsyncSeriesBailHook } = require('tapable')

let hook = new AsyncSeriesBailHook(['name'])

console.time('time')
hook.tapPromise('fn1', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`fn1 => ${name}`) // fn1 => yhzzy
      resolve()
    }, 1000)
  })
})

hook.tapPromise('fn2', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`fn2 => ${name}`) // fn2 => yhzzy
      resolve('hook end')
    }, 2000)
  })
})

hook.tapPromise('fn3', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`fn3 => ${name}`) // 被熔断不执行
      resolve()
    }, 3000)
  })
})

hook.promise('yhzzy').then(function (msg) {
  console.log(msg) // hook end
  console.timeEnd('time') // time: 3.018s
})

AsyncSeriesWaterfallHook

const { AsyncSeriesWaterfallHook } = require('tapable')

let hook = new AsyncSeriesWaterfallHook(['name'])

console.time('time')
hook.tapPromise('fn1', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`fn1 => ${name}`) // fn1 => yhzzy
      resolve('tom')
    }, 1000)
  })
})

hook.tapPromise('fn2', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`fn2 => ${name}`) // fn2 => tom
      resolve('mike')
    }, 2000)
  })
})

hook.tapPromise('fn3', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`fn3 => ${name}`) // fn3 => mike
      resolve('jim')
    }, 3000)
  })
})

hook.promise('yhzzy').then(function (msg) {
  console.log(msg) // jim
  console.timeEnd('time') // time: 6.029s
})

AsyncSeriesLoopHook

const { AsyncSeriesLoopHook } = require('tapable')

let hook = new AsyncSeriesLoopHook(['name'])

let count = 0
console.time('time')
hook.tapPromise('fn1', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`fn1 => ${name}`)
      resolve()
    }, 1000)
  })
})

hook.tapPromise('fn2', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`fn2 => ${name}`)
      if (++count === 2) {
        count = 0
        resolve()
      }
      resolve('1')
    }, 2000)
  })
})

hook.tapPromise('fn3', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`fn3 => ${name}`)
      resolve()
    }, 3000)
  })
})

hook.promise('yhzzy').then(function (msg) {
  console.timeEnd('time')
})

// fn1 => yhzzy
// fn2 => yhzzy
// fn1 => yhzzy
// fn2 => yhzzy
// fn3 => yhzzy
// time: 9.028s

异步并行钩子

异步同时执行

AsyncParalleHook

const { AsyncParallelHook } = require('tapable')

const hook = new AsyncParallelHook(['name', 'age'])

console.time('time')
hook.tapAsync('fn1', (name, age, callback) => {
  setTimeout(() => {
    console.log(`fn1 => ${name}: ${age}`)
    callback()
  }, 1000)
})

hook.tapAsync('fn2', (name, age, callback) => {
  setTimeout(() => {
    console.log(`fn2 => ${name}: ${age}`)
    callback()
  }, 2000)
})

hook.callAsync('yhzzy', 18, function () {
  console.timeEnd('time')
})

// fn1 => yhzzy: 18
// fn2 => yhzzy: 18
// time: 2.006s

AsyncParalleBailHook

const { AsyncParallelHook } = require('tapable')

const hook = new AsyncParallelHook(['name', 'age'])

console.time('time')
hook.tapAsync('fn1', (name, age, callback) => {
  setTimeout(() => {
    console.log(`fn1 => ${name}: ${age}`)
    callback()
  }, 1000)
})

hook.tapAsync('fn2', (name, age, callback) => {
  setTimeout(() => {
    console.log(`fn2 => ${name}: ${age}`)
    callback('err')
  }, 2000)
})

hook.tapAsync('fn3', (name, age, callback) => {
  setTimeout(() => {
    console.log(`fn3 => ${name}: ${age}`)
    callback()
  }, 3000)
})

hook.callAsync('yhzzy', 18, function () {
  console.timeEnd('time')
})

// fn1 => yhzzy: 18
// fn2 => yhzzy: 18
// time: 2.005s
// fn3 => yhzzy: 18

模拟tapable源码实现

以同步钩子syncHook和异步钩子asyncParallelHook为例

tapable的所有钩子都会继承一个Hook基类,然后通过HookCodeFactory类来生成注册事件时的回调函数,继而执行

基类Hook
Hook是所有钩子都会去继承的父类,我们将源码提炼一下,把主要的环节留下

class Hook {
  constructor(args = []) {
    this.args = args // 存放参数数组
    this.taps = []  // 将来用于存放组装好的tap注册事件对象
    this._x = undefined  // 将来在HookCodeFactory中会给_x赋值为一个数组,里面包含从taps中遍历提取的每一个注册函数
  }

  // 注册同步钩子
  tap(options, fn) {
    // 将注册时传进来的名称转换为键值对形式
    if (typeof options === 'string') {
      options = { name: options }
    }
    options = Object.assign({ fn }, options)  // { fn:... name:fn1 }

    // 调用以下方法将组装好的options添加至taps数组
    this._insert(options)
  }

  // 注册异步钩子
  tapAsync(options, fn) {
    // 将注册时传进来的名称转换为键值对形式
    if (typeof options === 'string') {
      options = { name: options }
    }
    options = Object.assign({ fn }, options)  // { fn:... name:fn1 }

    // 调用以下方法将组装好的options添加至taps数组
    this._insert(options)
  }

  _insert(options) {
    this.taps[this.taps.length] = options
  }

  // 同步方法执行
  call(...args) {
    // 创建将来要具体执行的函数代码结构
    let callFn = this._createCall('sync')
    // 调用上述的函数(args传入进去)
    return callFn.apply(this, args)
  }

  // 异步方法执行
  callAsync(...args) {
    let callFn = this._createCall('async')
    return callFn.apply(this, args)
  }

  _createCall(type) {
    // 调用具体钩子类的compile方法,进行具体执行的函数代码组装
    return this.compile({
      taps: this.taps,
      args: this.args,
      type: type
    })
  }
}

module.exports = Hook

Hook代码组装工厂

class HookCodeFactory {
  args({ after, before } = {}) {
    let allArgs = this.options.args
    if (before) allArgs = [before].concat(allArgs)
    if (after) allArgs = allArgs.concat(after)
    return allArgs.join(',')  // ["name", "age"] => name, age
  }
  header() {
    return `"use strict";var _context;var _x = this._x;`
  }
  content(type) {
    let code = ''
    switch (type) {
      case 'sync':
        for (let i = 0; i < this.options.taps.length; i++) {
          code += `var _fn${i} = _x[${i}];_fn${i}(${this.args()});`
        }
        break
      case 'async':
        code = `var _counter = ${this.options.taps.length};var _done = (function () {
          _callback();
        });`
        for (let i = 0; i < this.options.taps.length; i++) {
          code += `var _fn${i} = _x[${i}];_fn${i}(${this.args()}, (function () {
            if (--_counter === 0) _done();
          }));`
        }
        break
    }
    return code
  }
  setup(instance, options) {  // 先准备后续需要使用到的数据
    this.options = options  // 这里的操作在源码中是通过 init 方法实现,而我们当前是直接挂在了 this 身上
    instance._x = options.taps.map(o => o.fn)   // this._x = [f1, f2, ....]
  }
  create() { // 核心就是创建一段可执行的代码体然后返回
    // fn = new Function("name, age", "var _x = this._x, var _fn0 = _x[0]; _fn0(name, age);")
    const type = this.options.type
    const argsParams = type === 'sync' ? '' : { after: '_callback' }
    return new Function(
      this.args(argsParams),
      this.header() + this.content(this.options.type)
    )
  }
}

module.exports = HookCodeFactory

具体钩子类
SyncHook.js

const Hook = require('./Hook.js')
const HookCodeFactory = require('./HookCodeFactory.js')

const factory = new HookCodeFactory()

class SyncHook extends Hook {
  constructor(args) {
    super(args)
  }

  compile(options) {  // {taps: [{}, {}], args: [name, age], type: type}
    factory.setup(this, options)
    return factory.create(options)
  }
}

module.exports = SyncHook

AsyncParallelHook.js

const Hook = require('./Hook.js')
const HookCodeFactory = require('./HookCodeFactory.js')

const factory = new HookCodeFactory()

class AsyncParallelHook extends Hook {
  constructor(args) {
    super(args)
  }

  compile(options) {  // {taps: [{}, {}], args: [name, age], type: type}
    factory.setup(this, options)
    return factory.create(options)
  }
}

module.exports = AsyncParallelHook
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值