tapable听说了很久,终于下定决心系统学习一下
Q1:tapable解决的问题?
- tapable是个独立的库
- webpack中大量使用了这个库
- tapable主要是用来处理事件,解决的问题有点类似EventEmitter,不过功能更加强大
Q2:tapable方法有哪些?
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
复制代码
好的,方法一共是上述这么多,第一眼看过去,懵逼树下你和我,所以我们还是一点点来,一个个的分析、学习和了解
Q3:啥是SyncHook?
先来个使用的例子,例如前端开发者需要掌握哪些技能?
step1:首先我们要明确群体是前端开发
const {SyncHook}= require('tapable');
const FrontEnd = new SyncHook();
复制代码
ok,就是上面这两句,我们创建了个FrontEnd前端开发
step2:前端开发需要掌握哪些技能,例如webpack、react对吧
FrontEnd.tap('webpack',()=>{
console.log("get webpack")
});
FrontEnd.tap('react',()=>{
console.log("get react")
});
复制代码
ok,上面的tap就是用来绑定事件的,为前端开发添加了两个技能
step3:技能需要学习才能掌握,所以我们要有学习的动作
FrontEnd.learn=()=>{
FrontEnd.call()
};
FrontEnd.learn();
复制代码
step4:查看执行结果
get webpack
get react
复制代码
可以看到,通过上面的调用,我们的前端开发已经学会了react、webpack
step5:传参
前面知道FrontEnd这个群体,需要学react、webpack,但落到个人角度,究竟哪一个开发者掌握这些技能了呢?
const {SyncHook}= require('tapable');
const FrontEnd = new SyncHook();
FrontEnd.tap('webpack',(name)=>{
console.log(name+" get webpack")
});
FrontEnd.tap('react',(name)=>{
console.log(name+" get react")
});
FrontEnd.start=(name)=>{
FrontEnd.call(name)
};
FrontEnd.start('xiaoming');
复制代码
修改前面的代码,添加参数,预期是输出xxx get react
step6: 查看输出结果
undefined get webpack
undefined get react
复制代码
最终结果是undefined,也就是参数没传进去
step7:为SyncHook添加约定参数
这是因为const FrontEnd = new SyncHook();
创建SyncHook的时候没有约定参数,只要为其添加参数即可,如下:
const {SyncHook}= require('tapable');
const FrontEnd = new SyncHook(['name']);// 添加参数约定
FrontEnd.tap('webpack',(name)=>{
console.log(name+" get webpack")
});
FrontEnd.tap('react',(name)=>{
console.log(name+" get react")
});
FrontEnd.start=(name)=>{
FrontEnd.call(name)
};
FrontEnd.start('xiaoming');
复制代码
最终输出:
xiaoming get webpack
xiaoming get react
复制代码
SyncHook总结
- SyncHook目前来看比较像订阅发布
- 就像jquery中的add、fire方法,只不过这里是tap、call
Q4:SyncHook如何实现?
SyncHook实现比较简单,就是最简单的订阅发布
class SyncHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tap(name,task){
this.tasks.push(task);
}
call(...args){
const param = args.slice(0,this.limit.length);
this.tasks.forEach(item=>item(...param));
}
}
复制代码
- limit是用来做参数校验的
- tasks用来收集订阅
- tap方法用来想tasks中添加方法
- call方法,先检验参数,然后再执行所有的已订阅方法
总结:原理比较简单,没有太多技术含量,主要就是一个同步的钩子函数
Q5:啥是SyncBailHook?
熔断机制,如果前一个事件return true
,则不再执行下一个,还是前面的例子:
const {SyncBailHook} =require('tapable');
const FrontEnd = new SyncBailHook(['name']);
FrontEnd.tap('webpack',(name)=>{
console.log(name+" get webpack ")
});
FrontEnd.tap('react',(name)=>{
console.log(name+" get react")
});
FrontEnd.start=(...args)=>{
FrontEnd.call(...args)
};
FrontEnd.start('xiaoming');
复制代码
此时,把函数从SyncHook换成SyncBailHook,执行的结果没有任何区别
but,思考一下,学习很容易会学不下去,所以修改一下我们的例子:
const {SyncBailHook} =require('tapable');
const FrontEnd = new SyncBailHook(['name']);
FrontEnd.tap('webpack',(name)=>{
console.log(name+" get webpack ")
return '学不动了啊!';
});
FrontEnd.tap('react',(name)=>{
console.log(name+" get react")
});
FrontEnd.start=(...args)=>{
FrontEnd.call(...args)
};
FrontEnd.start('xiaoming');
复制代码
此时仅输出:
xiaoming get webpack
复制代码
后面的react没有执行
总结:
- SyncBailHook主要解决的问题是条件阻塞
- 当订阅事件符合某一判断时,不再执行下面的流程
- 应用场景,场景不断深入的场景,a、a+b、a+b+c、a+b+c+d这种场景
Q6:SyncBailHook如何实现?
SyncBailHook也十分简单,还是之前那个例子:
class SyncBailHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tap(name,task){
this.tasks.push(task);
}
call(...args){
const param = args.slice(0,this.limit.length);
this.tasks.some(item=>item(...param));// 只改了一行
}
}
复制代码
可以看到,和上面SyncHook十分相似,无非就是把执行函数forEach,换成some,因为some是阻塞式执行,当返回true,则不会执行后面的内容
Q7:啥是SyncWaterfullHook?
还是先来个使用的例子,例如前端,技能都是一个个学的,要学完webpack再学react,例如:
const {SyncWaterfallHook} = require('tapable');
const FrontEnd = new SyncWaterfallHook(['name']);
FrontEnd.tap('webpack',(name)=>{
console.log(name+" get webpack ")
return '学完webpack了,该学react了';
});
FrontEnd.tap('react',(name)=>{
console.log(name+" get react")
});
FrontEnd.start=(...args)=>{
FrontEnd.call(...args)
};
FrontEnd.start('xiaoming');
复制代码
此时输出:
xiaoming get webpack
学完webpack了,该学react了 get react
复制代码
- SyncWaterfallHook会将前一个任务的执行结果,传递给后一个
- 主要使用场景是处理逻辑之间相互依赖
- 实际效果和redux中的compose方法一毛一样
Q8:SyncWaterfullHook如何实现?
class SyncWaterfallHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tap(name,task){
this.tasks.push(task);
}
call(...args){
const param = args.slice(0,this.limit.length);
const [first,...others] = this.tasks;
const ret = first(...param);
others.reduce((pre,next)=>{
return next(pre);
},ret)
}
}
复制代码
SyncWaterfallHook实现也比较简单
- 完全按照redux的compose来实现就行
- 第一步,取出第一个执行,并拿到结果ret
- 第二步,将结果ret,当作reduce的参数传递进去
- 第三步,遍历,不断把参数传给下一个函数
总结:SyncWaterfallHook主要还是用于函数之间对结果存在依赖的场景
Q9:啥是SyncLoopHook?
还是前面的例子,如果一次学不懂一门技术,那就要多学几遍,例如:
const FrontEnd = new SyncLoopHook(['name']);
let num = 0;
FrontEnd.tap('webpack',(name)=>{
console.log(name+" get webpack ")
return ++num === 3?undefined:'再学一次';
});
FrontEnd.tap('react',(name)=>{
console.log(name+" get react")
});
FrontEnd.start=(...args)=>{
FrontEnd.call(...args)
};
FrontEnd.start('xiaoming');
复制代码
上面执行的结果是:
xiaoming get webpack
xiaoming get webpack
xiaoming get webpack
xiaoming get react
复制代码
- SyncLoopHook任务能够执行多次
- 返回undefined则停止执行,返回非undefined则继续执行当前任务
总结:主要场景是同一任务,需要执行多次
Q10:SyncLoopHook如何实现?
class SyncLoopHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tap(name,task){
this.tasks.push(task);
}
call(...args){
const param = args.slice(0,this.limit.length);
let index = 0;
while(index<this.tasks.length){
const result = this.tasks[index](...param);
if(result === undefined){
index++;
}
}
}
}
复制代码
- 上面的实现是通过计数
- 如果结果不为undefined则下标index不移动
- 如果结果为undefined则下标index增加
也可以换doWhile来实现
class SyncLoopHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tap(name,task){
this.tasks.push(task);
}
call(...args){
const param = args.slice(0,this.limit.length);
this.tasks.forEach(task=>{
let ret;
do{
ret = task(...param);
}while(ret!=undefined)
})
}
}
复制代码
- 这种实现没有下标概念了
- 直接遍历tasks任务组,如果任务组中某一个任务执行的结果不是undefined则再次执行
总结:SyncLoopHook这个使用场景相对较少,不过了解一下也好
Q11:啥是AsyncParralleHook?
前面了解的都是同步hook,更关键的是异步hook
举个例子,同学小王说去学前端了,但你也不知道他什么时候学完,只有他学完告诉你,你才知道他学完了,例:
const {AsyncParallelHook} = require('tapable');
const FrontEnd = new AsyncParallelHook(['name']);
FrontEnd.tapAsync('webpack',(name,cb)=>{
setTimeout(() => {
console.log(name+" get webpack ")
cb();
}, 1000);
});
FrontEnd.tapAsync('react',(name,cb)=>{
setTimeout(() => {
console.log(name+" get react")
cb();
}, 1000);
});
FrontEnd.start=(...args)=>{
FrontEnd.callAsync(...args,()=>{
console.log("end");
})
};
FrontEnd.start('小王');
复制代码
最终输出:
小王 get webpack
小王 get react
end
复制代码
- AsyncParralleHook是异步并行钩子
- 使用场景,例如同时发起对两个接口的请求
- 注意:这次注册事件,不再是tap了,而是tapAsync
- 注意:这次的事件执行,不再是call了,而是callAsync
- 可以看出tapable中区分了同步、异步的订阅和发布
- 注意:想要让所有异步执行完成后,接收到通知,需要执行cb()
Q12:AsyncParralleHook如何实现?
class AsyncParallelHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapAsync(name,task){
this.tasks.push(task);
}
callAsync(...args){
const finalCallBack = args.pop();
const param = args.slice(0,this.limit.length);
let index = 0;
const done=()=>{
index++;
if(index === this.tasks.length){
finalCallBack();
}
}
this.tasks.forEach(item=>item(...param,done))
}
}
复制代码
- AsyncParallelHook最简单就是通过计数
- 在实例上添加一个计数器
- 然后遍历tasks,当任务成功个数与任务总数相同时,执行finalCallBack
总结:AsyncParallelHook解决的问题和promise.all类似,都是用于解决异步并行的问题
Q13:AsyncParralleHook(2)如何使用promise?
前面虽然用:AsyncParralleHook能够解决异步,但并没有使用primise,也没有类promise的概念
const {AsyncParallelHook} = require('tapable');
const FrontEnd = new AsyncParallelHook(['name']);
FrontEnd.tapPromise('webpack',(name)=>{
return new Promise((resolve)=>{
setTimeout(() => {
console.log(name+" get webpack ")
resolve();
}, 1000);
})
});
FrontEnd.tapPromise('react',(name,cb)=>{
return new Promise((resolve)=>{
setTimeout(() => {
console.log(name+" get react ")
resolve();
}, 1000);
})
});
FrontEnd.start=(...args)=>{
FrontEnd.promise(...args).then(()=>{
console.log("end");
})
};
FrontEnd.start('小王');
复制代码
调用上面的api后,输出:
小王 get webpack
小王 get react
end
复制代码
- 注意:此时绑定事件的方法叫做tapPromise
- 注意:此时执行事件的方法叫做promise
总结:
- tapable共有三种事件绑定方法:tap、tapAsync、tapPromise
- tapable共有三种事件执行方法:call、callAsync、promise
Q14:AsyncParralleHook(2)promise版如何实现?
class AsyncParallelHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapPromise(name,task){
this.tasks.push(task);
}
promise(...args){
const param = args.slice(0,this.limit.length);
const tasks = this.tasks.map(task=>task(...param));
return Promise.all(tasks)
}
}
复制代码
- 核心就是实现两个方法,tapPromise和promise
- tapPromise其实和之前的tap没有明显区别(简单实现的问题)
- promise的话,其实就是返回一个Promise.all
Q15:啥是AsyncParallelBailHook?
AsyncParallelBailHook这个钩子和前面的钩子不太一样 按前面的例子来讲:
- 同学小王说去学前端了,但你也不知道他什么时候学完,只有他学完告诉你,你才知道他学完了
- 小王学了webpack,学崩了,告诉了你
- 你听说小王学崩了,你就以为他学不下去了,你就对大家伙说,小王学崩了
- 但是小王同时也学了react却咬牙学完了
- 虽然学完了,但你已经对外宣布小王崩了,很打脸,所以就当不知道了
这就是AsyncParallelBailHook处理的事情
const {AsyncParallelBailHook} = require('tapable');
const FrontEnd = new AsyncParallelBailHook(['name']);
FrontEnd.tapPromise('webpack',(name)=>{
return new Promise((resolve,reject)=>{
setTimeout(() => {
console.log(name+" get webpack ")
reject('小王学崩了!');
}, 1000);
})
});
FrontEnd.tapPromise('react',(name,cb)=>{
return new Promise((resolve)=>{
setTimeout(() => {
console.log(name+" get react ")
resolve();
}, 2000);
})
});
FrontEnd.start=(...args)=>{
FrontEnd.promise(...args).then(()=>{
console.log("end");
},(err)=>{
console.log("听说:",err)
})
};
FrontEnd.start('小王');
复制代码
上面代码执行结果是:
小王 get webpack
听说: 小王学崩了!
小王 get react
复制代码
- 上面例子,第一个并行任务返回了reject
- reject只要不是undefined,就会直接进入promise.all的catch
- 异步任务,react还是会执行,但成功后没有处理了
再看一个例子:
const {AsyncParallelBailHook} = require('tapable');
const FrontEnd = new AsyncParallelBailHook(['name']);
FrontEnd.tapPromise('webpack',(name)=>{
return new Promise((resolve,reject)=>{
setTimeout(() => {
console.log(name+" get webpack ")
reject();
}, 1000);
})
});
FrontEnd.tapPromise('react',(name,cb)=>{
return new Promise((resolve)=>{
setTimeout(() => {
console.log(name+" get react ")
resolve();
}, 2000);
})
});
FrontEnd.start=(...args)=>{
FrontEnd.promise(...args).then(()=>{
console.log("end");
},(err)=>{
console.log("听说:",err)
})
};
FrontEnd.start('小王');
复制代码
和上面就改了1行,就是reject内容为空,此时输出:
小王 get webpack
小王 get react
end
复制代码
- 此时即便调用了reject也不会进入到catch
- reject返回空,后面的任务也会照常执行
总结:
- AsyncParallelBailHook,如果返回真值,则直接会走进catch
- 无论返回结果是什么,所有任务都会执行
- 主要场景是,并行请求3个接口,随便哪一个返回结果都行,只要返回了,就对返回进行处理(走catch)
- 如果用来处理同步,则和SyncBailHook效果一样
- 如果处理tapSync,则遇到return true最终的callback不会执行
- 如果处理promise,则遇到rejcet(true),则直接进入catch
Q16:AsyncParallelBailHook如何实现?
这个AsyncParallelBailHook真真烧脑了好一会
class AsyncParallelBailHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapPromise(name,task){
this.tasks.push(task);
}
promise(...args){
const param = args.slice(0,this.limit.length);
const tasks = this.tasks.map(task=>{
return new Promise((resolve,reject)=>{
task(...param).then((data)=>{
resolve(data);
},(err)=>{
err? reject(err):resolve();
});
})
});
return Promise.all(tasks)
}
}
复制代码
- 正常情况下,promise.all中任意一个任务reject,就会进入统一的catch
- 但我们需要的是根据reject的值来判断是否走如catch
- 所以我们在原有task外,再包一层promise
- 如果reject值为真,则执行reject
- 如果reject值为假,则执行resolve,就当什么也没发生
Q17:啥是AsyncSeriesHook?
前面讲的是异步并行,现在该说异步串行了,例如小王,学完webpack才去学的react,你也不知道他什么时候学完,但他学完一个就会告诉你一下,例:
const {AsyncSeriesHook} = require('tapable');
const FrontEnd = new AsyncSeriesHook(['name']);
console.time('webpack');
console.time('react');
FrontEnd.tapPromise('webpack',(name,cb)=>{
return new Promise((resolve,reject)=>{
setTimeout(() => {
console.log(name+" get webpack ")
console.timeEnd('webpack');
resolve();
}, 1000);
})
});
FrontEnd.tapPromise('react',(name,cb)=>{
return new Promise((resolve)=>{
setTimeout(() => {
console.log(name+" get react ")
console.timeEnd('react');
resolve();
}, 1000);
})
});
FrontEnd.start=(...args)=>{
FrontEnd.promise(...args).then(()=>{
console.log("end");
})
};
FrontEnd.start('小王');
复制代码
上面代码执行结果:
小王 get webpack
webpack: 1010.781ms
小王 get react
react: 2016.598ms
end
复制代码
- 两个异步任务,变成了串行
- 从时间能够得出,两个1s的异步的任务,串行后总时间变成了2s
总结:AsyncSeriesHook解决的问题是异步串行,例如node的os.cpus()有限,可以把任务分批次执行,这样对性能有保障
Q18:AsyncSeriesHook如何实现?
class AsyncSeriesHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapPromise(name,task){
this.tasks.push(task);
}
promise(...args){
const param = args.slice(0,this.limit.length);
const [first,...others] = this.tasks;
return others.reduce((pre,next)=>{
return pre.then(()=>next(...param))
},first(...param))
}
}
复制代码
- 实现核心就是promise串行
- 取出第一个任务,执行拿到promise实例,然后通过reduce遍历
Q19:啥是AsyncSeriesBailHook?
还是前面的例子,如果小王学前端,学了webapck就彻底放弃了,那后面的react也就不用学了
const {AsyncSeriesBailHook} = require('tapable');
const FrontEnd = new AsyncSeriesBailHook(['name']);
console.time('webpack');
console.time('react');
FrontEnd.tapPromise('webpack',(name,cb)=>{
return new Promise((resolve,reject)=>{
setTimeout(() => {
console.log(name+" get webpack ")
console.timeEnd('webpack');
reject('小王彻底放弃了');
}, 1000);
})
});
FrontEnd.tapPromise('react',(name,cb)=>{
return new Promise((resolve)=>{
setTimeout(() => {
console.log(name+" get react ")
console.timeEnd('react');
resolve();
}, 1000);
})
});
FrontEnd.start=(...args)=>{
FrontEnd.promise(...args).then(()=>{
console.log("end");
}).catch((err)=>{
console.log("err",err)
})
};
FrontEnd.start('小王');
复制代码
上面代码输出:
小王 get webpack
webpack: 1010.518ms
err 小王彻底放弃了
复制代码
- 上面的代码只执行到webpack
- AsyncSeriesBailHook,任务如果return,或者reject,则阻塞了
场景:主要是异步串行,如果某一个任务执行的结果reject或者return,那么后面的都将不再执行
Q20:AsyncSeriesBailHook如何实现?
class AsyncSeriesBailHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapPromise(name,task){
this.tasks.push(task);
}
promise(...args){
const param = args.slice(0,this.limit.length);
const [first,...others] = this.tasks;
return new Promise((resolve,reject)=>{
others.reduce((pre,next,index,arr)=>{
return pre
.then(()=>next(...param))
.catch((err=>{
arr.splice(index,arr.length-index);
reject(err);
})).then(()=>{
(index+1 === arr.length) && resolve();
})
},first(...param))
})
}
}
复制代码
AsyncSeriesBailHook实现难度要高很多
- 首先在reduce外再包一层promise
- 当遇到任何一个子任务进入catch的时候,则将reduce的第四个参数arr切割,使其无法再向下进行,也就是停止reduce的继续
- 同时所有promise后面再添加一个后置then,用来检测是否全部执行完成
- 为什么使用index+1,是因为后置then肯定是最后一个任务,但遍历index还处于上一个下标,所以只要加1就好
Q21:啥是AsyncSeriesWaterfallHook?
SyncWaterFallHook前面已经了解过了,就是前一个执行完的结果会传递给下一个执行函数,和AsyncSeriesWaterfallHook的区别就是,一个是同步一个是异步
具体来说,例如只有一本教材,小王学完,小张才能学
const FrontEnd = new AsyncSeriesWaterfallHook(['name']);
FrontEnd.tapAsync('webpack',(name,cb)=>{
setTimeout(() => {
console.log(name+" get webpack ")
cb(null,'小李');
}, 1000);
});
FrontEnd.tapAsync('webpack',(name,cb)=>{
setTimeout(() => {
console.log(name+" get webpack ")
cb(null,'小张');
}, 1000);
});
FrontEnd.tapAsync('webpack',(name,cb)=>{
setTimeout(() => {
console.log(name+" get webpack ")
cb(null,'小红');
}, 1000);
});
FrontEnd.start=(...args)=>{
FrontEnd.callAsync(...args,(data)=>{
console.log("全学完了",)
})
};
FrontEnd.start('小王');
复制代码
上面代码,最终输出:
小王 get webpack
小李 get webpack
小张 get webpack
全学完了
复制代码
总结:这个的用法和SyncWaterFallHook的用法一致
Q22:AsyncSeriesWaterfallHook如何实现?
class AsyncSeriesWaterfallHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapAsync(name,task){
this.tasks.push(task);
}
callAsync(...args){
const param = args.slice(0,this.limit.length);
const finalCallBack = args.pop();
let index = 0;
const next = (err,data)=>{
const task = this.tasks[index];
if(!task)return finalCallBack();
if(index === 0){
task(...param,next)
}else{
task(data,next)
}
index++;
}
next();
}
}
复制代码
- 主要是通过封装一个回调函数next
- 然后不断调用任务队列中的任务,调用的时候,再传递相同的回调函数进去
prmise版本的实现如下:
class AsyncSeriesWaterfallHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapPromise(name,task){
this.tasks.push(task);
}
promise(...args){
const param = args.slice(0,this.limit.length);
const [first,...others] = this.tasks;
return others.reduce((pre,next)=>{
return pre.then((data)=>{
return data?next(data):next(...param);
})
},first(...param))
}
}
复制代码
- promise的实现要相对简单一些
- 主要去看then方法中是否有内容,如果有的话,则传递个下一个函数,如果没有,则用初始参数
总结
- tapable的各AsyncHook都同时支持tap、tapAsync、tapPromise
- tapable主要解决的是事件流转的问题,各个Hook使用的场景各有不同
- tapable主要应用在webpack的各个生命周期中,具体的实践还需要结合webpack原理去看