webpack 的强大,离不开 Tapable 库的功劳。在整个打包构建的流程中,会涉及很多个阶段,每个阶段都会调用相应的钩子。钩子里可以挂上插件,当触发钩子的 call 方法时,里面的插件就会一一被通知执行,而 Tapable 库就是提供这些钩子。
基本的流程如下:
-
注册:通过基类实例的 tap/ tapAsync/ tapPromise 方法注册对应的 hook 函数。
-
触发:通过 call/ callAsync/ promise 方法去触发钩子。
-
生成执行代码:不同触发方法传入 compile 的 type 值不同。compile 再根据不同过的 type 生成可执行函数。
-
执行。
钩子的基类可以分为 Hook 和 HookCodeFactory 。前者主要是收集并处理挂载在钩子上的 taps ,后者会根据前者返回的 options 生成执行钩子的代码。
以下篇幅主要是以 Hook 的基础种类的使用方法展开。
同步钩子
顾名思义就是按照时间顺序执行的钩子,代表有 SyncHook、SyncBailHook、SyncWaterfallHook。
SyncHook
class People{
constructor(){
this.setInfo = SyncHook(["name","age"]); // 表示这个hook需要多少个参数
}
print(name, age){
this.setInfo.call(name, age)
}
}
let tomato = new People();
tomato.setInfo.tap('basicInfo', (...params)=>{
let [name, age] = params;
console.log(my name is <span class="hljs-subst">${name}</span>, I am <span class="hljs-subst">${age}</span> years old)
})
tomato.print('tomato', 12)
// output
// my name is tomato, I am 12 years old// output
// my name is tomato, I am 12 years old
SyncHook 的原理有点类似于 发布订阅 模式。tap 去注册消费者,call 去通知消费者执行
call 的实现方法有点绕,主要是通过拼接字符串的形式创建 new Function,再将编译好的 function 赋给 call,最后再按顺序依次执行插件。以下为简略版的发布订阅。
class SyncHook{
constructor() {
this.taps = [];
}
tap(options, fn){
options = Object.assign({ type, fn }, options);
let i = this.taps.length;
while(i){
//...
}
this.taps[i] = options;
}
call(...arg){
let args = arg.join(", ");
let fn = new Function(args, /** 拼接函数字符串 **/)
for (let j = this.taps.length - 1; j >= 0; j--) {
// ..按顺序遍历执行 fn
}
}
}
SyncBailHook
SyncBailHook 的特点在于如果遇到第一个返回为非 undefined 的事件则停止,不会继续走下去。
class People{
constructor(){
this.hook = {
setName: SyncHook(["name"]),
address: SyncBailHook(["name"])
}
}
printAddress(name){
this.hook.address.call(name);
}
}
let tomato = new People();
tomato.hook.address.tap('province',(name) =>{
console.log("广东省")
})
tomato.hook.address.tap('city',(name) =>{
console.log("深圳市");
// 匹配到市就停止了
if(name.toString().indexOf("市")) return true;
})
tomato.hook.address.tap('area',(name) =>{
console.log("南山区");
})
tomato.printAddress("深圳市")
// output
// 广东省
// 深圳市// output
// 广东省
// 深圳市
SyncWaterfallHook
SyncWaterfallHook 有点类似于数组的 reduce 方法,上一个执行返回的结果作为第一个参数传给下一个插件。
class People{
constructor(){
this.hook = {
income: SyncWaterfallHook(["num"])
}
}
rest(number){
this.hook.income.call(number);
}
}
let tomato = new People();
tomato.hook.income.tap('Eat',(num) =>{
console.log(伙食支出:1000);
return num-1000
})
tomato.hook.income.tap('Clothes',(num) =>{
console.log(服饰支出:1000);
return num-1000
})
tomato.hook.income.tap('all',(num) =>{
console.log(剩余:<span class="hljs-subst">${num}</span>);
})
tomato.rest(5000)
// output
// 伙食支出:1000
// 服饰支出:1000
// 剩余:3000// output
// 伙食支出:1000
// 服饰支出:1000
// 剩余:3000
异步钩子
如果回调函数存在异步,则需要用到异步的钩子。代表有 AsyncSeriesHook、AsyncSeriesBailHook、AsyncSeriesWaterfallHook、AsyncParallelHook、AsyncParallelBailHook
AsyncSeriesHook
AsyncSeriesHook 是异步串行钩子,也就是异步函数会按顺序执行。
class People{
constructor(){
this.hook = {
name: AsyncSeriesHook(["name"])
}
}
sayName(name){
return this.hook.name.promise(name);
}
}
let tomato = new People();
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(my name is <span class="hljs-subst">${name}</span>)
resolve()
}, 2000)
})
})
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(my name2 is <span class="hljs-subst">${name}</span>)
resolve()
}, 1000)
})
})
tomato.sayName('tomato').then(() => { console.log("end") })
// output 总共花费3s
// my name is tomato
// my name2 is tomato
// end// output 总共花费3s
// my name is tomato
// my name2 is tomato
// end
AsyncSeriesBailHook
AsyncSeriesBailHook 也是异步串行钩子,如果遇到第一个返回值,则会立刻跳到最终的回调函数,不会继续执行接下来的插件。
class People{
constructor(){
this.hook = {
name: AsyncSeriesBailHook(["name"])
}
}
sayName(name){
return this.hook.name.promise(name);
}
}
let tomato = new People();
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(my name is <span class="hljs-subst">${name}</span>)
resolve(name) // !!重点
}, 2000)
})
})
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(my name2 is <span class="hljs-subst">${name}</span>)
resolve()
}, 1000)
})
})
tomato.sayName('tomato').then(() => { console.log("end") })
// output
// my name is tomato
// end// output
// my name is tomato
// end
AsyncSeriesWaterfallHook
AsyncSeriesWaterfallHook 也是异步串行钩子,会将前一个的返回值传给下一个。
AsyncParallelHook
AsyncParallelHook 是异步并行钩子,等所有注册的异步任务都执行完,才会执行最后的回调,而所有的异步任务几乎是同时进行的。
class People{
constructor(){
this.hook = {
name: AsyncParallelHook(["name"])
}
}
sayName(name){
return this.hook.name.promise(name);
}
}
let tomato = new People();
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1 <span class="hljs-subst">${name}</span>,new Date())
resolve()
}, 2000)
})
})
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(2 <span class="hljs-subst">${name}</span>, new Date())
resolve()
}, 1000)
})
})
tomato.sayName('tomato').then(() => { console.log("end",new Date()) })
// output
// 2 tomato Wed Oct 28 2020 16:55:52 GMT+0800 (中国标准时间)
// 1 tomato Wed Oct 28 2020 16:55:53 GMT+0800 (中国标准时间)
// end Wed Oct 28 2020 16:55:53 GMT+0800 (中国标准时间)// output
// 2 tomato Wed Oct 28 2020 16:55:52 GMT+0800 (中国标准时间)
// 1 tomato Wed Oct 28 2020 16:55:53 GMT+0800 (中国标准时间)
// end Wed Oct 28 2020 16:55:53 GMT+0800 (中国标准时间)
AsyncParallelBailHook
AsyncParallelBailHook 类似于 AsyncParallelHook,但是当第一个插件执行完且 resolve 不传 undefined 则立即熔断,直接调用最终的回调函数,其他异步的插件正常进行。
- 如果所有插件的 resolve 不传参数,则效果和 AsyncParallelHook 一样
- 如果第一个插件的 resolve 为 undefined,则下一个 resolve 不会 undefined 的插件的值会传到最终的回调函数。
- 如果 reject 不传参数,则不会执行到最终的 catch
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`1 ${name}`,new Date())
resolve()
}, 1000)
})
})
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`2 ${name}`, new Date())
resolve(name+'2')
}, 2000)
})
})
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`3 ${name}`, new Date())
}, 2000)
})
})
tomato.sayName('tomato').then((res) => { console.log("end", res) }).catch((err) => console.log("oops!",err))
// 1 tomato 2020-12-01T03:20:43.539Z
// 2 tomato 2020-12-01T03:20:44.545Z
// end tomato2
// 3 tomato 2020-12-01T03:20:44.545Z// 1 tomato 2020-12-01T03:20:43.539Z
// 2 tomato 2020-12-01T03:20:44.545Z
// end tomato2
// 3 tomato 2020-12-01T03:20:44.545Z