tapable用法简介
我们都知道,webpack的插件机制是基于tapable库实现的,tapable库提供了9种钩子函数,这里按照钩子的执行机制(并行、串行、是否熔断、是否异步、注册/触发方式),绘制了下面表格,看起来有点复杂,实际上只需要关注钩子的执行机制,事件的注册/触发机制即可。
下面我们通过一个个实例和配套的图解,来介绍一下tapable库中所有钩子的用法
SyncHook钩子
执行机制:钩子被触发后,按照钩子函数的注册顺序依次同步执行
例子图解:假设A,B,C进行接力跑比赛,这个场景和SyncHook钩子
的执行逻辑十分相似,A跑完了B跑,B跑完了C跑,按照安排好的顺序依次触发执行!
测试代码:
// 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就被熔断了,将不再执行抢红包的动作。
测试代码:
// 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分结束统计!
测试代码:
// 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左右
测试代码:
// 统计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左右
测试代码:
// 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左右
测试代码:
// 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后执行!
测试代码:
// 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左右,且输出最终的比赛得分
测试代码:
// 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所有钩子的执行逻辑
、注册与触发方式
。大家在使用的时候可以根据自己的实际需求去选用,为了方便大家学习,将所有的钩子总结如下表所示:
钩子类型、注册与触发方式
执行机制
后续会更新tapable的实现原理,有兴趣的的同学可以点赞关注起来~~!