首发于我的博客:https://www.ahonn.me/post/58如果你在阅读webpack之前不了解tapable,你可能会一头雾水,那么到底是什么可点击的,它有什么用?本文主要介绍tapable的使用及相关实现。通过学习tapable,可以进一步了解webpack的插件机制。以下内容基于tapablev1.1.3版本。tapable是一个类似于Node.js中的EventEmitter的库,但更侧重于自定义事件的触发和处理。webpack通过tapable将实现与流程解耦,所有具体实现都以插件的形式存在。基本使用想要了解tapable的实现,就必须知道tapable的用法和使用姿势。Tapable主要提供同步和异步钩子。让我们从一个简单的同步挂钩开始。同步钩子SyncHook以最简单的SyncHook为例:const{SyncHook}=require('tapable');consthook=newSyncHook(['name']);hook.tap('hello',(name)=>{console.log(`hello${name}`);});hook.tap('helloagain',(name)=>{console.log(`hello${name},again`);});hook.call('ahonn');//helloahonn//helloahonn,again你可以看到当我们执行hook.call('ahonn'),前面hook.tap(name,callback)中的回调函数会依次执行。通过SyncHook创建同步钩子,使用tap注册回调,然后调用call触发。这是tapable提供的各种hooks中比较简单的一种,通过EventEmitter也可以轻松实现这种效果。此外,tapable还提供了很多有用的同步钩子:SyncBailHook:类似于SyncHook,当执行过程中注册的回调返回非undefined时,会停止不执行。SyncWaterfallHook:至少接受一个参数,上一个注册回调的返回值将作为下一个注册回调的参数。SyncLoopHook:类似于SyncBailHook,但在执行过程中回调返回非undefined时,会继续再次执行当前回调。异步钩子tapable中除了同步执行的钩子外,还有一些异步钩子。两个最基本的异步钩子是AsyncParallelHook和AsyncSeriesHook。其他异步钩子在这两个钩子的基础上增加了一些流程控制,类似于SyncBailHook和SyncHook的关系。AsyncParallelHookAsyncParallelHook,顾名思义,就是并行执行的异步钩子。当所有注册的异步回调并行执行时,执行callAsync或promise中的函数。const{AsyncParallelHook}=require('tapable');consthook=newAsyncParallelHook(['name']);console.time('cost');hook.tapAsync('hello',(name,cb)=>{setTimeout(()=>{console.log(`hello${name}`);cb();},2000);});hook.tapPromise('helloagain',(name)=>{returnnewPromise((resolve)=>{setTimeout(()=>{console.log(`hello${name},again`);resolve();},1000);});});hook.callAsync('ahonn',()=>{console.log('done');console.timeEnd('cost');});//helloahonn,again//helloahonn//done//cost:2008.609ms//或者通过hook.promise()调用//hook.promise('ahonn').then(()=>{//console.log('done');//console.timeEnd('cost');//});可以看到AsyncParallelHook比SyncHook复杂的多。SyncHook等同步钩子只能通过tap注册,而异步钩子也可以通过tapAsync或tapPromise注册回调。执行方式。异步钩子没有call方法,注册的回调是通过callAsync和promise方法触发的。两者的区别如上面的代码所示。AsyncSeriesHook如果要顺序执行异步函数,显然AsyncParallelHook是不适合的。所以tapable提供了另一个基本的异步钩子:AsyncSeriesHook。const{AsyncSeriesHook}=require('tapable');consthook=newAsyncSeriesHook(['name']);console.time('cost');hook.tapAsync('hello',(name,cb)=>{setTimeout(()=>{console.log(`hello${name}`);cb();},2000);});hook.tapPromise('helloagain',(name)=>{returnnewPromise((resolve)=>{setTimeout(()=>{console.log(`hello${name},again`);resolve();},1000);});});hook.callAsync('ahonn',()=>{console.log('done');console.timeEnd('cost');});//helloahonn//helloahonn,again//done//cost:3011.162ms以上示例代码与AsyncParallelHook的示例代码几乎相同,除了钩子是使用newAsyncSeriesHook()实例化的。通过AsyncSeriesHook,可以顺序执行注册的回调。另外,注册和触发的用法是一样的。同样,异步钩子也有一些带流控的钩子:AsyncParallelBailHook:当执行过程中注册的回调返回非undefined时,会直接执行callAsync或promise中的函数(由于是并行执行,其他注册的回调仍然会被执行)。AsyncSeriesBailHook:当执行过程中注册的回调返回非undefined时,会直接执行callAsync或promise中的函数,不会执行后续注册的回调。AsyncSeriesWaterfallHook:类似于SyncWaterfallHook,将上一次注册的异步回调执行后的返回值传递给下一次注册的回调。除了这些核心的hooks,其他的tapables也提供了一些功能,比如HookMap,MultiHook等,这里就不详细描述了,有兴趣的可以自行访问。如果想了解tapable的具体实现,一定要阅读相关的源码。限于篇幅,这里我们先阅读SyncHook的相关代码,看看相关的实现。其他钩子的思路大致相同。我们通过以下代码慢慢深入tapable的实现:const{SyncHook}=require('tapable');consthook=newSyncHook(['name']);hook.tap('hello',(name)=>{console.log(`hello${name}`);});hook.call('ahonn');Entry首先我们实例化SyncHook,通过package.json可以知道tapable的入口在/lib/index。js,这里导出了上面提到的同步/异步钩子。SyncHook对应的实现在/lib/SyncHook.js中。在这个文件中,我们可以看到SyncHook类的结构如下:classSyncHookexntendsHook{tapAsync(){...}tapPromise(){...}compile(options){...}}newSyncHook(),我们会调用对应实例的tap方法来注册回调。很明显,tap并不是在SyncHook中实现的,而是在父类中实现的。注册回调,可以看到tapablehook的大部分方法都是在/lib/Hook.js文件中的Hook类中实现的,包括tap、tapAsync、tapPromise、call、callAsync等方法。我们主要关注tap方法。我们可以看到,这个方法除了检查一些参数外,还调用了另外两个内部方法:_runRegisterInterceptors和_insert。_runRegisterInterceptors()是运行寄存器拦截器,我们暂时忽略它(拦截器见tapable#interception)。关注_insert方法:_insert(item){this._resetCompilation();让之前;if(typeofitem.before==='string')before=newSet([item.before]);elseif(Array.isArray(item.before)){before=newSet(item.before);}让阶段=0;if(typeofitem.stage==='number')stage=item.stage;让我=this.taps.length;while(i>0){i--;constx=this.taps[i];this.taps[i+1]=x;常量xStage=x.stage||0;if(before){if(before.has(x.name)){before.delete(x.name);继续;}if(before.size>0){继续;}}如果(xStage>阶段){继续;我++;休息;}this.taps[i]=item;}这里分为三部分,第一部分是this._resetCompilation(),这里主要是重置call、callAsync、promise这三个函数。至于为什么要这样做,后面再说,不过这里有一个眼睛。第二部分是一堆复杂的逻辑,主要是通过options中的before和stage来判断当前tap注册的回调在哪里,也就是提供优先级配置,默认添加到当前已有的this中。点击后。去掉before和stage相关的代码后,_insert变成了这样:让我=this.taps.length;this.taps[i]=item;}Trigger到目前为止还没有特别的show操作,我们继续看。我们注册回调后,就可以通过调用来触发了。我们可以通过Hook类的构造函数看出来。构造函数(args){如果(!Array.isArray(args))args=[];this._args=参数;this.taps=[];this.interceptors=[];this.call=this._call;这。promise=this._promise;this.callAsync=this._callAsync;this._x=undefined;}这时候我们可以发现call、callAsync、promise都指向下划线开头的同名函数。在文件的底部,我们看到以下代码:返回此[名称](...参数);};}Object.defineProperties(Hook.prototype,{_call:{value:createCompileDelegate("call","sync"),configurable:true,writable:true},_promise:{value:createCompileDelegate("promise","promise""),可配置:true,可写:true},_callAsync:{value:createCompileDelegate("callAsync","async"),可配置:true,可写:true}});这里可以看到lazyCompileHook这个函数实际上是在第一次执行调用的时候运行的,this函数会调用this._createCall('sync')生成一个新的函数来执行,生成的函数在后面再次调用该调用时才真正执行。这里我们其实可以理解调用tap时执行的this._resetCompilation()的功能。也就是说只要没有新的tap注册回调,call就会调用同一个函数(第一次调用call生成)。在执行一个新的tap注册回调后第一次调用call方法将重新生成该函数。其实我不太明白为什么要通过Object.defineProperties在原型链上添加方法。直接写在Hook类中效果应该是一样的。tapable在当前的v2.0.0beta版本中不再以这种方式实现,如果有人知道原因的话。在评论中让我知道。为什么需要重新生成函数?秘密就在this._createCall('sync')中的this.complie()中。_createCall(type){returnthis.compile({taps:this.taps,interceptors:this.interceptors,args:this._args,type:type});}编译函数this.complie()在Hook中没有实现,我们跳回SyncHook,我们可以看到:compile(options){factory.setup(this,options);returnfactory.create(options);}这里出现了一个工厂,我们可以看到这个工厂是上面SyncHookCodeFactory类的一个实例,只是在SyncHookCodeFactory中实现了内容。于是我们继续查看父类HookCodeFactory(lib/HookCodeFactory.js)中的设置和创建。这里setup函数将Hook类传递过来的options.taps中的回调函数(调用tap时传入的函数)赋值给SyncHook中的this._x:setup(instance,options){instance._x=options.taps.map(t=>t.fn);}并在factory.create()执行后返回,这里可以知道create()返回的返回值一定是一个函数(用于调用调用)。查看对应的源码,create()方法的实现有一个开关,我们重点关注'sync'这个case。删除冗余代码后,我们可以看到create()方法是这样的:create(options){this.init(options);让fn;switch(this.options.type){case"sync":fn=newFunction(this.args(),'"usestrict";\n'+this.header()+this.content({onError:err=>`throw${err};\n`,onResult:result=>`return${result};\n`,resultReturns:true,onDone:()=>"",rethrowIfPossible:true}));休息;}this.deinit();returnfn;}这里可以看到使用TonewFunction()生成函数并返回,这是tapable的关键。通过实例化SyncHook时传入的参数名列表和后面注册的回调信息,生成一个函数来执行它们。对于不同的tapablehook,最大的区别就是这里生成的函数不同。如果是带流控的hook,生成的代码也会有相应的逻辑。这里我们在returnfn之前添加fn.toString()以查看生成的函数的样子:functionanonymous(name){'usestrict';变量_上下文;var_x=this._x;var_fn0=_x[0];_fn0(name);}由于我们的代码比较简单,所以生成的代码也很简单。主要逻辑是获取this._x中的第一个函数,传入参数执行。如果我们通过tapbeforecall注册回调。然后生成的代码会对应的获取到_x[1]来执行第二个注册的回调函数。至此,newSyncHook()->tap->call的整个过程就结束了。调用执行时会缓存主要的两个有趣点,根据已知信息生成不同的函数供调用执行。其他钩子的操作过程基本上都是类似的。生成不同流程控制的具体细节这里不再赘述。读者可以自行阅读源码(具体逻辑在SyncHookCodeFactory类的create方法中)。总结一下,webpack通过tapable巧妙的hook设计,很好的将实现和流程解耦,值得学习。或许下次写轮子需要插件机制的时候,可以借鉴一些webpack的做法。但是,tapablegenerator函数的部分看起来不是很优雅。或许JavaScript可以支持元编程,或许可以实现的更好?本文如有理解或表达错误,欢迎评论告诉我。谢谢阅读。
