您现在的位置是:网站首页> 内容页

显微镜下的webpack4:灵魂tapable,终于搞懂钩子系列!

  • 拉斯维加斯游戏官网网址
  • 2019-10-10
  • 74人已阅读
简介简介大家在看webpack源码的时候,有没有感觉像再看天书,似乎没有办法一个文件比如webpack.js从头看到尾。感觉webpack的跳跃性很强,完全不知道程序在运行的时候,发生了什

简介

大家在看webpack源码的时候,有没有感觉像再看天书,似乎没有办法一个文件比如webpack.js从头看到尾。感觉webpack的跳跃性很强,完全不知道程序在运行的时候,发生了什么。完全不清楚这个事件是什么时候发生的,比如loader是什么时候执行的,plugin又是什么时候出现的。webpack的程序错综复杂,完全迷失在程序之中。这究竟是为什么呢?其实很简单!因为webpack的灵魂Tapable!这个机制使得webpack异常的灵活,它有一句经典的话——Everything is a plugin!。由此可见webpack是靠插件堆积起来的。而实现这个插件机制的就是Tabable!

Events

webpack的灵魂Tapable,有点类似于nodejs的Events,都是注册一个事件,然后到了适当的时候触发。这里的事件触发是这样绑定触发的,通过on方法,绑定一个事件,emit方法出发一个事件。Tapable的机制和这类似,也是tap注册一个事件,然后call执行这个事件。

const EventEmitter = require("events");const myEmitter = new EventEmitter();//on的第一个参数是事件名,之后emit可以通过这个事件名,从而触发这个方法。//on的第二个参数是回掉函数,也就是此事件的执行方法myEmitter.on("newListener", (param1,param2) => { console.log("newListener",param1,param2)});//emit的第一个参数是触发的事件名//emit的第二个以后的参数是回调函数的参数。myEmitter.emit("newListener",111,222);

Tapable究竟为何物

如果我们把Tapable的实例对象比作一颗参天大树,那么的每一根树枝就是一个挂载的hook(钩子),也就是Tapable之中给每一个事件分门别类的机制,比如编译(compile.js)这个对象中,又运行(run)的钩子,有构建(make)的钩子,这些钩子就像树枝一样,组成了一棵树的骨干,然后每个树枝上的树叶就是每个钩子上面挂载的函数方法。树枝(钩子)越多,树叶(函数)越多,此树越茂密(程序越复杂)。

当然这只是一个简易的理解。实际上,webpack中不止有一棵树,每棵树之间还有错综复杂的关系。比如有些方法如compilation.js中的一些方法,就要等compile.js中的make这个钩子执行之后才会执行。那么我们就从了解Tapable中钩子的用法,来理解webpack中tapable。

以工作日为例,了解Tapable的用法

即使webpack中的每颗tapable的树之间有错综复杂的关系,整个程序都有一个逻辑线,也就是游戏中的主线剧情,我们先构建我们工作日的主线剧情。

主线剧情

让我们来回一下,我们的日常工作日,应该大多数分成3个阶段,上班前,上班中和下班后,这3个时间段。这三个时间段,我用了3中钩子类型,普通型,流水型和熔断型。按照文档他们的解释是这样的:

普通型basic:这个比较好理解就是按照tap的注册顺序一个个向下执行。流水型water:这个相对于basic的区别就是,虽然也是按照tap的顺序一个个向下执行,但是如果上一个tap有返回值,那么下一个tap的传入参数就是上一个tap的返回值。熔断型bail:这个相对于water的区别就是,如果返回了null以外的值,就不继续执行了。

是不是感觉一个事件的订阅发布怎么可以分出这么多类型?不要急,每个类型都有他的作用!

钩子的语法一般都是new 钩子类型Hook([参数名1,参数名2,参数名3]),这里的数组是只是提示你传入参数有几个,给了名字只是为了可读性,如果你想写一个别人看不懂的可以这样new SyncHook(["a","b","c"]),这里要注意这个参数名的类型是字符串。如果没有提前准备号需要传入的参数,后续挂函数的时候,就无法传入参数了。这个设计应该是为了日后好打理,告诉其他开发者,我传入的参数类型。

class MyDaily { constructor() { this.hooks = { beforeWork: new SyncHook(["getUp"]), atWork: new SyncWaterfallHook(["workTask"]), afterWork: new SyncBailHook(["activity"]) }; } tapTap(){ //此处是行为 } run(){ this.hooks.beforeWork.call() this.hooks.atWork.call() this.hooks.afterWork.call() }}

一天我们不可能什么事都不做,所以给钩子上加点事,tap事情。先来点必然发生的,正常的上班族,自由职业不在考虑范围内。早上我们会做什么呢?穿衣服出门是必备的,不穿衣服没法出门,不出门没法上班。到了工作岗位,来点工作任务吧,比如我们需要做个ppt,然后用这个ppt去开会。下班后,本来想回家的,结果佳人有约,果然不回家。

tapTap(){ this.hooks.beforeWork.tap("putOnCloth",()=>{ console.log("穿衣服!") }) this.hooks.beforeWork.tap("getOut",()=>{ console.log("出门!") }) this.hooks.atWork.tap("makePPT",()=>{ console.log("做PPT!") return "你的ppt" }) this.hooks.atWork.tap("meeting",(work)=>{ console.log("带着你的"+work+"开会!") }) this.hooks.afterWork.tap("haveADate",()=>{ console.log("约会咯!") return "约会真开心~" }) this.hooks.afterWork.tap("goHome",()=>{ console.log("溜了溜了!") })}

从上述我们可以看到通过主演剧情了解到各种同步钩子的用法,可能难以理解就是熔断型的钩子,这个钩子的存在意义就是,可以中断一系列的事情,比如有地方出错了,或者不需要进行下一步的操作我们就可以及时结束。

那么如果我们做的事情都是异步的,每一个事件之间都有联系,那么我们就不能用同步的方法了。这个时候我们可以将sync钩子替换成async的钩子。

async相对于sync多了一个callback的机制,就是这样的:

this.hooks.beforeWork.tapAsync("putOnCloth",(params,callback)=>{ console.log("穿衣服!") callback();//此处无callback,则getOut这个挂载的函数便不会运行})this.hooks.beforeWork.tapAsync("getOut",(params,callback)=>{ console.log("出门!") callback()//此处无callback,则beforeWork这个钩子的回调函数不会执行})this.hooks.beforeWork.callAsync("working",err=>{ console.log(err+" end!")//如果最后一个tap的函数没有callback则不会执行})

这里我们可以将callback当作next函数,也就是下一个tap的函数的意思。以及如果当前tap的函数报错,则可以在callback中加入错误的原因,那么接下来的函数便不会运行,也就是这样callback("errorReason"),那么就直接回调用当前钩子的callAsync绑定的函数。

this.hooks.beforeWork.tapAsync("putOnCloth",(params,callback)=>{ console.log("穿衣服!") callback("error");此处加入了错误原因,那么直接callAsync,抛弃了getOut})this.hooks.beforeWork.tapAsync("getOut",(params,callback)=>{//直接skip了 console.log("出门!")})this.hooks.beforeWork.callAsync("working",err=>{ console.log(err+" end!")//error end!直接打出错误原因。})

小tips

大家发现没有,Async和sync的区别在于Async通过callback来和后续的函数沟通,sync则是通过return一个值来做交流。所以,Async自带sync中bail类型的钩子。我曾经做了一个无聊的统计,因为钩子太多了,我写了一个代码遍历了webpack这个项目,得出了所有钩子的使用情况,结果如下所示:

SyncHook 69SyncBailHook 63SyncWaterfallHook 36SyncLoopHook 0AsyncParallelHook 1AsyncParallelBailHook 0AsyncSeriesHook 14AsyncSeriesBailHook 0AsyncSeriesWaterfallHook 5

但是我发现AsyncSeriesBailHook竟然是0的时候,我很震惊,现在知道原因了,因为从作用上来说他和异步钩子的共能本身就重叠了,所以同理AsyncParallelBailHook这个平行执行的bail类型的钩子也是0 。bail在Async中功能重复,因次用的很少。

言归正传,既然AsyncSeriesHook的callback通过第一个err参数来判断是否异步成功,不成功则直接callAsync回调。那么water类型的该如何传递参数?我们都知道water和basic的区别就在于basic每个异步tap之间并无参数传递,而water则是参数传递。很简单,在err后面再加一个参数,作为下一个tap的传入值。

this.hooks.atWork.tapAsync("makePPT",(work,callback)=>{ console.log("做PPT!") callback("没做完 ","你的ppt")//第一个参数是err,上交你的报错,第二个参数是你自定义要下一个tap处理的参数。如果有err,则忽略此参数。})this.hooks.atWork.tapAsync("meeting",(work,callback)=>{//因为ppt没做完,所以开不了会 console.log("带着"+work+"开会!") callback()})this.hooks.atWork.callAsync("working",err=>{//没做完来这里领罚了。 console.log(err+" end!")})

支线剧情

我们的日常生活!才不会这么单调,怎么会一路顺顺利利地走下来呢?每天都有不同的精彩啊!小插曲肯定少不了。

那么我们就要相办法将支线剧情插入主线剧情之中了。这个时候一个MyDaily的类已经放不下我们的精彩生活了。

以下班之后的精彩为例,我们不一定会直接回家也有可能约会蹦迪什么的。所以这里我们new一个名为Activity的类。假设我们的夜生活有两个活动,一个派对活动,一个回家。那么派对活动肯定有个流程,我们就用熔断型的,为什么呢!不开心了接下来就都别执行了!回家吧!回家的这个活动就是简单的钩子。

class Activity { constructor() { this.hooks = { goParty:new SyncBailHook(), goHome:new SyncHook() }; } prepare(){ this.hooks.goParty.tap("happy",()=>{ console.log("happy party") }) this.hooks.goParty.tap("unhappy",()=>{ console.log("unhappy") return "go home" }) this.hooks.goParty.tap("play",()=>{ console.log("continue playing") }) this.hooks.goHome.tap("goHome",()=>{ console.log("I"m going to sleep") }) } start(){ this.hooks.goParty.call() this.hooks.goHome.call() }}

然后我们要将这个单独的类挂到MyDaily的下面,毕竟这也是日常的一部分虽然非正式关卡。我们可以在工作结束自开始准备晚上的活动,等到一下班就开始我们丰富的夜生活。这个时候我们可以在钩子的回调函数中触发另一个类中的钩子状态,激活或着运行。

class MyDaily { constructor() { this.hooks = { .... }; this.activity=new Activity()//实例化Activity } run(){ ... this.hooks.atWork.callAsync("working",res=>{ this.activity.prepare()//下班了大家可以躁动起来了 }) this.hooks.afterWork.callAsync("activity",err=>{ this.activity.start()//去浪咯! }) }}

总结

我在这里只是举了一个小例子,带大家理解tapable是什么。因为理解了tapable的特性,我们才能在之后有办法理解webpack的机制,因为这种钩子套钩子的原因,我们很难看懂webpack的源代码。下一篇文章我会带大家看懂webpack的主线剧情和主要支线剧情(loader&plugin)的流程!

文章评论

Top