tapable用法图解-你真的了解webpack的核心原理吗?还不快学起来!

898 阅读8分钟

tapable用法简介

我们都知道,webpack的插件机制是基于tapable库实现的,tapable库提供了9种钩子函数,这里按照钩子的执行机制(并行、串行、是否熔断、是否异步、注册/触发方式),绘制了下面表格,看起来有点复杂,实际上只需要关注钩子的执行机制,事件的注册/触发机制即可。 image.png

下面我们通过一个个实例和配套的图解,来介绍一下tapable库中所有钩子的用法

SyncHook钩子

执行机制:钩子被触发后,按照钩子函数的注册顺序依次同步执行

例子图解:假设A,B,C进行接力跑比赛,这个场景和SyncHook钩子的执行逻辑十分相似,A跑完了B跑,B跑完了C跑,按照安排好的顺序依次触发执行!

屏幕快照 2022-03-28 下午3.26.17.png 测试代码

// Resgiter.js
import {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncSeriesHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook
} from 'tapable'

export default class Car {
  constructor() {
    this.hooks = {
      syncHook: new SyncHook(),
      syncBailHook: new SyncBailHook(['sum']),
      syncWaterfallHook: new SyncWaterfallHook(['score']),
      syncLoopHook: new SyncLoopHook(),
      asyncSeriesHook: new AsyncSeriesHook(),
      asyncParallelHook: new AsyncParallelHook(),
      asyncParallelBailHook: AsyncParallelBailHook(),
      asyncSeriesBailHook: new AsyncSeriesBailHook(),
      asyncSeriesWaterfallHook: new AsyncSeriesWaterfallHook(['score']) // 标注一下,要传参数啦
    }
  }

  syncHook() {
    // 基本类型钩子
    return this.hooks.syncHook.call()
  }

  syncBailHook(sum) {
    // 同步熔断钩子
    return this.hooks.syncBailHook.call(sum)
  }

  syncWaterfallHook(score) {
    // 同步瀑布钩子
    return this.hooks.syncWaterfallHook.call(score)
  }

  syncLoopHook() {
    // 同步循环钩子
    return this.hooks.syncLoopHook.call()
  }

  asyncSeriesHook(callback) {
    // 异步串行钩子
    return this.hooks.asyncSeriesHook.callAsync(callback)
  }

  asyncParallelHook() {
    // 异步串行钩子
    return this.hooks.asyncParallelHook.promise()
  }

  asyncParallelBailHook() {
    // 异步并行钩子
    return this.hooks.asyncParallelBailHook.promise()
  }

  asyncSeriesBailHook() {
    // 异步并行熔断钩子
    return this.hooks.asyncSeriesBailHook.promise()
  }

  asyncSeriesWaterfallHook() {
    // 异步并行瀑布钩子
    return this.hooks.asyncSeriesWaterfallHook.promise()
  }
}

// index.js
import Resgiter from './Resgiter'

const resgiter = new Resgiter()
// A,B,C接力跑
resgiter.hooks.syncHook.tap('grabRedEnvelopePligin', sum => {
  console.log('A,跑完了!')
})
resgiter.hooks.syncHook.tap('grabRedEnvelopePligin', sum => {
  console.log('B,跑完了!')
})
resgiter.hooks.syncHook.tap('grabRedEnvelopePligin', sum => {
  console.log('C,跑完了!')
})
//执行顺序: A,跑完了 => B,跑完了!=> C,跑完了!
resgiter.syncHook()

SyncBailHook

执行机制SyncBailHook钩子被触发后,执行顺序与基本类型钩子一致,不同的是其加了一层保险逻辑,即如果任意一个钩子函数的返回值为非undefined,整个钩子的执行过程会立即中断,之后注册的钩子函数将不会再执行

例子图解:假设A,B,C,D在玩抢红包游戏,一共三个红包,A手速快先抢到,返回红包还有的信息return undefined,B第二抢到,返回红包还有的信息return undefined,C第三抢到,并返回红包已经抢完了的信息return false,此时D就被熔断了,将不再执行抢红包的动作。

image.png

测试代码

// index.js
import Resgiter from './Resgiter'
const resgiter = new Resgiter()

resgiter.hooks.syncBailHook.tap('grabRedEnvelopePligin', sum => {
  console.log('A,抢到了,真开心!')
})
resgiter.hooks.syncBailHook.tap('grabRedEnvelopePligin', sum => {
  console.log('B,抢到了,真开心!')
})
resgiter.hooks.syncBailHook.tap('grabRedEnvelopePligin', sum => {
  if (sum >= 3) {
    console.log('C,抢到了,真开心!')
  }

  if (sum < 4) {
    console.log('C,抢完了!')
    return false // 返回值不是undefind,就会熔断,D被熔断,不在执行
  }
})
resgiter.hooks.syncBailHook.tap('grabRedEnvelopePligin', sum => {
  if (sum >= 4) {
    console.log('D,抢到了,真开心!')
  }

  if (sum < 5) {
    console.log('D,抢完了!')
    return false
  }
})

resgiter.syncBailHook(3) // 发三个红包

SyncWaterfallHook

执行机制SyncBailHook钩子被触发后,执行顺序与基本类型钩子一致,不同的是其关注钩子函数的返回值,会依次传递函数的返回值给下一个钩子函数

例子图解:假设我们需要计算某团队A,B,C三人在一次比赛中的总得分,A得分结果出来以后,传递给B,B计算出A和B的得分以后,传递给C,C计算出三人的总得分,三个钩子分别依赖上一个钩子的返回值,这就是SyncBailHook钩子的执行逻辑

测试代码

// index.js
import Resgiter from './Resgiter'
const resgiter = new Resgiter()

// 某团队包含ABC三人,计算再一次比赛中三人的总得分
resgiter.hooks.syncWaterfallHook.tap('countTotalScorePligin', score => {
  console.log('A得10分!')
  return score + 10
})

resgiter.hooks.syncWaterfallHook.tap('countTotalScorePligin', score => {
  console.log('B得12分!')
  return score + 12
})

resgiter.hooks.syncWaterfallHook.tap('countTotalScorePligin', score => {
  console.log('C得11分!')
  return score + 11
})

resgiter.hooks.syncWaterfallHook.tap('countTotalScorePligin', score => {
  console.log(`A,B,C三人的总得分为:${score}`)
})

resgiter.syncWaterfallHook(0)

SyncLoopHook

执行机制SyncLoopHook钩子被触发后,执行顺序与基本类型钩子一致,不同的是如果任意一个钩子函数的返回值为非undefined,那么会立即重新从头开始执行所有的钩子函数,直到所有钩子函数的返回值都为undefined

例子图解:假设在一场比赛中,要求A,B都得5分才能结束比赛,这是可以用SyncLoopHook钩子来实现,开始统计比分以后,先统计A的比分,当A比分小于5分时,会发出A得分不合格的信号,return分数,当A比分等于5分时,发出A已得分合格的信号,return undefined,此时开始统计B的得分,每次先看A的分数是否还满足5分,即从头开始执行所有钩子,然后在判断B的得分,直到A,B均满足5分结束统计!

image.png

测试代码

// index.js
import Resgiter from './Resgiter'
const resgiter = new Resgiter()

// A、B分别得5分才能完成任务
let i = 1
let j = 1
resgiter.hooks.syncLoopHook.tap('countTotalScorePligin', score => {
  if (i < 5) {
    i = i + 1
    console.log('A', i)
    return i
  } else {
    console.log('A得5分!')
  }
})
resgiter.hooks.syncLoopHook.tap('countTotalScorePligin', score => {
  if (j < 5) {
    j = j + 1
    console.log('B', j)
    return j
  } else {
    console.log('B得5分!')
  }
})

resgiter.syncLoopHook(0)

AsyncSeriesHook

执行机制AsyncSeriesHook钩子被触发后,串行执行所有注册的异步钩子函数,所有钩子函数执行完毕后,执行最终的回调函数

例子图解:我们可以通过统计回调函数的执行时间来验证AsyncSeriesHook钩子的执行逻辑,假设A执行需要耗时1s,B执行也需要1s,那根据AsyncSeriesHook钩子的串行运行机制,则最终回调函数的执行时间将会来到2s左右

WeChatcb1e23f758696bbc462725f8d3df07ef.png

测试代码

// 统计A,B完成任务所花费的总时间
// index.js
import Resgiter from './Resgiter'
const resgiter = new Resgiter()

console.time('timer')

resgiter.hooks.asyncSeriesHook.tapAsync('countTotalScorePligin', callback => {
  setTimeout(() => {
    console.log('A,我执行需要1s!')
    callback()
  }, 1000)
})
resgiter.hooks.asyncSeriesHook.tapAsync('countTotalScorePligin', callback => {
  setTimeout(() => {
    console.log('B等A执行完毕后开始执行,B执行也需要1s!')
    callback()
  }, 1000)
})

resgiter.asyncSeriesHook(() => {
  console.log('执行最终的回调,A,B完成任务所花费的总时间为:')
  console.timeEnd('timer') // 总时间在两秒左右
})

AsyncParallelHook

执行机制AsyncParallelHook钩子被触发后,并发执行所有注册的异步钩子函数,所有钩子函数执行完毕后,执行最终的回调函数,这也是同步钩子与异步钩子的区别

例子图解:同样,我们可以通过统计回调函数的执行时间来验证AsyncParallelHook钩子的执行逻辑,假设A执行需要耗时1s,B执行也需要1s,那根据AsyncSeriesHook钩子的并行运行机制,则最终回调函数的执行时间将会为1s左右

image.png

测试代码

// index.js
import Resgiter from './Resgiter'
const resgiter = new Resgiter()

// 统计A,B完成任务所花费的总时间
console.time('timer')
resgiter.hooks.asyncParallelHook.tapPromise('countTotalScorePligin', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('A,我执行需要1s!')
      resolve()
    }, 1000)
  })
})

resgiter.hooks.asyncParallelHook.tapPromise('countTotalScorePligin', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('B,我执行需要1s!')
      resolve()
    }, 1000)
  })
})

resgiter.asyncParallelHook().then(() => {
  console.log('执行最终的回调,A,B完成任务所花费的总时间为:') // 总时间在一秒左右
  console.timeEnd('timer')
})

AsyncSeriesBailHook

执行机制AsyncSeriesBailHook钩子被触发后,串行执行所有注册的异步钩子函数,如果任意一个钩子函数的返回值为非undefined,整个钩子的执行过程会立即中断,并立马执行最终的回调函数,熔断之后注册的钩子函数将不会再执行

例子图解:我们依然通过最终回调函数的执行时间来验证AsyncSeriesBailHook钩子的执行逻辑,假设A执行任务需要1秒,B执行任务需要3秒,但是A执行完毕以后将返回值抛了出去,这时B就会被熔断,直接执行最终的回调函数,那最终函数的执行时间就在1s左右

image.png

测试代码

// index.js
import Resgiter from './Resgiter'
const resgiter = new Resgiter()

// A,B,C按顺序执行任务,碰到熔断即执行熔断
console.time('timer')

resgiter.hooks.asyncSeriesBailHook.tapPromise('countTotalScorePligin', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('A,我执行需要1s!') // B已经被熔断,不会影响最后结果的执行时机,但是其定时器已经被开启,最终还是会执行打印
      resolve()
    }, 3000)
  })
})

resgiter.hooks.asyncSeriesBailHook.tapPromise('countTotalScorePligin', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('B,我执行需要3s!') // B已经被熔断,不会影响最后结果的执行时机,但是其定时器已经被开启,最终还是会执行打印
      resolve('B')
    }, 1000)
  })
})

resgiter.hooks.asyncSeriesBailHook.tapPromise('countTotalScorePligin', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('C,我执行需要5s!') // B已经被熔断,不会影响最后结果的执行时机,但是其定时器已经被开启,最终还是会执行打印
      resolve('C')
    }, 5000)
  })
})

resgiter.asyncSeriesBailHook().then(data => {
  console.timeEnd('timer') // 总时间在一秒左右
  console.log('最终的结果!', data)
})

AsyncParallelBailHook

执行机制AsyncParallelBailHook钩子被触发后,并行执行所有注册的异步钩子函数,如果任意一个钩子函数的返回值为非undefined,整个钩子的执行过程会立即中断,并立马执行最终的回调函数。熔断之前启动的钩子函数会继续执行完毕。

例子图解:还是通过统计回调函数的执行时间来验证AsyncParallelBailHook钩子的执行机制,A执行需要1s,B执行需要三秒,A,B同时开始执行,A在1s执行完毕以后,返回执行结果A(不是undefined),此时B被熔断,会立即执行最终的回调函数,所以回调函数会在1s左右执行,但是由于AsyncParallelBailHook的并行执行机制,B已经开始执行,虽然被熔断,但还是会在3s后执行! image.png

测试代码

// index.js
import Resgiter from './Resgiter'
const resgiter = new Resgiter()

console.time('timer')
// A完成任务以后,就立马执行最终结果
resgiter.hooks.asyncParallelBailHook.tapPromise('countTotalScorePligin', () => {
  // 第一个注册的插件完成后,立马执行最终的回掉函数,只有第一个注册的回调才能熔断其他注册的回调
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('A,我执行需要1s!')
      resolve('A') // 值必须为非undefined
    }, 1000)
  })
})

resgiter.hooks.asyncParallelBailHook.tapPromise('countTotalScorePligin', () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('B,我执行需要3s!') // B已经被熔断,不会影响最后结果的执行时机,但是其定时器已经被开启,最终还是会执行打印
      resolve()
    }, 3000)
  })
})

resgiter.asyncParallelBailHook().then(data => {
  console.timeEnd('timer') // 总时间在一秒左右
  console.log('最终的结果!', data)
})

AsyncSeriesWaterfallHook

执行机制AsyncSeriesWaterfallHook钩子被触发后,串行执行所有注册的异步钩子函数,会依次传递函数的返回值给下一个钩子函数,并最终执行回调函数

例子图解:假设我们统计A,B执行任务的时间和得分,就可以使用AsyncSeriesWaterfallHook钩子,A执行任务需要1s,然后将执行结果传递给B,B执行任务需要1s,且计算A,B的总等分,执行完毕后,将执行结果传递给最终的回调函数,由于AsyncSeriesWaterfallHook钩子的串行运行机制,所以最终回调函的执行时间在2s左右,且输出最终的比赛得分

image.png

测试代码

// index.js
import Resgiter from './Resgiter'
const resgiter = new Resgiter()

// B依赖A的执行结果
console.time('timer')

resgiter.hooks.asyncSeriesWaterfallHook.tapPromise('countTotalScorePligin', score => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('A,我执行需要1s!')
      console.log('A得10分')
      resolve(10)
    }, 1000)
  })
})

resgiter.hooks.asyncSeriesWaterfallHook.tapPromise('countTotalScorePligin', score => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('B,我执行需要1s!')
      console.log('B得10分')
      resolve(score + 10)
    }, 1000)
  })
})

resgiter.asyncSeriesWaterfallHook(0).then(data => {
  console.timeEnd('timer') // 总时间在一秒左右
  console.log('A,B的总分为:', data)
})

总结

以上就是tapable所有钩子的执行逻辑注册与触发方式。大家在使用的时候可以根据自己的实际需求去选用,为了方便大家学习,将所有的钩子总结如下表所示:

钩子类型、注册与触发方式

image.png 执行机制

image.png

后续会更新tapable的实现原理,有兴趣的的同学可以点赞关注起来~~!