硬核解析 Webpack 事件流核心!

Posted 全栈修仙之路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了硬核解析 Webpack 事件流核心!相关的知识,希望对你有一定的参考价值。

大佬,他会逐步介绍每个钩子,并分析其源码实现。是 Tapable 所提供的最简单的钩子,它是一个同步钩子。

本小节的内容会比较长,因为会介绍到很多钩子们共用的方法。学习完 SyncHook 钩子的实现,再去分析其它钩子的源码会轻松很多。

初始化 SyncHook 后,可以通过调用实例的 tap 方法来注册事件,调用 call 方法按注册顺序来执行回调:

在英文中有“窃听”的意思,用它作为订阅事件的方法名,还挺形象和俏皮。

模块的源码(简略版)如下:

实例化后其实就是一个 Hook 类的实例对象,并带上了一个自定义的 compile 方法。顾名思义,可以猜测 compile 方法是最终调用 call 时所执行的接口。

我们先不分析 compile 的实现,就带着对它的猜想,来看看 Hook 类的实现。

钩子相关的代码。这里需要知道的是,其中带下划线的方法如 _tap_resetCompilation_insert 等都属于各钩子公用的内部方法,而像 tapcall 方法是 SyncHook 钩子专有的方法。

我们综合梳理一下 SyncHook 调用 tapcall 的逻辑。


⑴ tap 的流程

继续以前方示例代码为例子:

的执行流程大致如下:

它会先在 _tap 方法里构建配置项 options 对象:

传递给 _insert 方法,该方法做了两件事:

  • 调用 this._resetCompilation() 重置 this.call
  • 发布订阅模式的常规操作,提供一个数组(this.taps)用于订阅收集,把 options 放入该数组。
  • 之所以需要重置 this.call 方法,是因为 this.call 执行时会重写自己:

    之后可以正常调用 hook.call,需要重新赋值 this.call,避免它被调用一次就不能再正常执行了。


    ⑵ call 的流程

    this.call 一开始就重写了自己:

    的实现可以知道,this.call 被重写为 this.compile 的返回值:

    的猜想 —— SyncHook.js 中定义的 compile 方法是在 call 调用时执行的。我们回过头来看它的实现:

    定义的方法中分别调用了 SyncHookCodeFactory 实例的 setupcreate 方法,它们都是从父类 HookCodeFactory 继承过来的。

    的内容相对较多,不过没关系,我们暂时只看当前 hook.call 执行流程相关的代码段即可:

    方法会将当前已注册事件的回调统一放到数组 this._x 中,后续要触发所有订阅事件回调,只需要按顺序执行 this._x 即可。

    create 则通过 new Function(args, functionString) 构造了一个函数,该函数最终由被重写的 call 触发。

    所以这里我们只需要再确认下 this.content 方法执行后的返回值,就能知道最终 call 方法所执行的函数是什么。


    回看前面的代码,content 是在 SyncHook.js 中定义的:

    接着看 this.callTapsSeries 的实现:

    方法每次执行会生成和返回单个订阅事件执行代码字符串,例如:

    会遍历订阅数组并逐次调用 callTap,最后将全部订阅事件的执行代码字符串拼接起来。

    理解 callTapsSeries 方法的关键点,是理解 current 变量在每次迭代前后的变化。

    假设存在 4 个订阅事件,则 current 的变化如下:

    遍历次序遍历索引current 初始值current 结束值
    第 1 次3onDone,即 ()=>""() => "_x[3]代码段"
    第 2 次2() => "_x[3]代码段"() => "_x[2]代码段 + _x[3]代码段"
    第 3 次1() => "_x[2]代码段 + _x[3]代码段"() => "_x[1]代码段 + _x[2]代码段 + _x[3]代码段"
    第 4 次0() => "_x[1]代码段 + _x[2]代码段 + _x[3]代码段"() => "_x[0]代码段 + _x[1]代码段 + _x[2]代码段 + _x[3]代码段"

    因此最后直接拼接 content() 就能得到完整的代码字符串。

    顺便我们也可以知道,onDone 参数是为了在遍历开始时,作为 current 的默认值使用的。

    示例:

    每次用户调用 syncHook.call 时,callTapsSeries 生成的函数片段字符串:

    方法中通过 new Function 创建为常规函数供 call 调用:

    里的事件回调会按顺序逐个执行。

    也是一个同步钩子,不同于 SyncHook 的地方是,如果某个订阅事件的回调函数返回了非 undefined 的值,那么会中断该钩子后续其它订阅回调的调用:

    的回调返回了 null,故中断了后续其它订阅回调的执行。

    的入口模块为 SyncBailHook.js,它相对于前一节的 SyncHook.js 而言,只是多了一个 content/callTapsSeries 方法的 onResult 传参:

    都是模板参数,它们执行后都会返回模板字符串,用于在 callTap 方法里拼接函数代码段。

    我们需要在调用 this.contentthis.callTapsSeries 的地方分别做点修改,让它们利用这个新增的模板参数,来实现 SyncBailHook 的功能。


    ⑴ content 调用处的改动

    调用 content 时不传 onResult 的简易处理:

    中最关键的实现,是利用了 current 的变化和传递,来实现各订阅事件回调执行代码字符串的拼接。

    对于 SyncBailHook 的需求,我们可以利用 onResultcurrent() 的模板包起来(把 onResult 的第三个参数设为 current 即可):

    时,我们都能获得如下模板:

    时,callTapsSeries 生成的函数片段字符串:

    依旧为同步钩子,不过它会把前一个订阅回调所返回的内容,作为第一个参数传递给后续的订阅回调:

    的模板参数,来生成 hook.call 最终调用的函数代码。

    SyncWaterfallHook 的实现也非常简单,只需要调整 onDoneonResult 两个模板参数即可:

    为订阅对象数组遍历时的初始化模板函数,执行后会生成 return $this._args[0];\\n 字符串。

    onResult 为订阅对象数组遍历时的非初始化模板函数,会判断上一个订阅回调返回值是否非 undefined,是则将 syncWaterfallHook.call 的第一个参数改为此返回值,再拼接上一次遍历生成的模板内容。

    示例

    每次用户调用 syncWaterfallHook.call 时,callTapsSeries 生成的函数片段字符串:

    表示如果存在某个订阅事件回调返回了非 undefined 的值,则全部订阅事件回调从头执行:

    “从头执行全部回调”的逻辑比较特殊,旧的方法已经无法满足该需求,所以 Tapable 为其新开了一个 callTapsLooping 方法来处理:

    方法的实现:

    在模板的外层包了个 do while 循环:

    生成的模板,只需要判断订阅回调返回值是否为 undefined,然后修改 _loop 即可。

    callTapsLooping 传入 callTapsSeriesonResult 参数完善了此块逻辑:

    示例

    每次用户调用 syncLoopHook.call 时,callTapsLooping 生成的函数片段字符串:

    表示一个异步串行的钩子,可以通过 hook.tapAsynchook.tapPromise 方法,来注册异步的事件回调。

    这些订阅事件的回调依旧是逐个执行,即必须等到上一个异步回调通知钩子它已经执行完毕了,才能开始下一个异步回调:

    来订阅事件的异步回调,可以通过执行最后一个参数来通知钩子“我已经执行完毕,可以接着执行后面的回调了”;

    对于使用 hook.tapPromise 来订阅事件的异步回调,需要返回一个 Promise,当其状态为 resolve 时,钩子才会开始执行后续其它订阅回调。

    另外需要留意下,AsyncSeriesHook 钩子使用新的 hook.callAsync 来执行订阅回调(而不再是 hook.call),且支持传入回调(最后一个参数),在全部订阅事件执行完毕后触发。

    模块的代码,结构和 SyncHook 是基本一样的:

    方法来处理异步回调中的错误。

    AsyncSeriesHook 钩子实现的关键点,是对其几个专用方法 hook.tapAsynchook.tapPromisehook.callAsync 的实现,我们先到 Hook.js 中查看它们的定义:

    一致,只是把订阅对象信息的 type 标记为 async/promise 罢了。


    我们继续到 HookCodeFactory 查阅 hook.callAsync 的处理:

    一样,hook.callAsync 执行时,先调用的是 create 方法里 case  "async" 的代码块。

    从传入 this.content 的参数可以猜测到,hook.callAsync 的函数模板里,会使用 Node Error First 异步回调的格式来书写相应逻辑:

    方法的改动 —— 在返回用户入参字符串的同时,可以通过传入 before/after,往返回的字符串前后再多插一个自定义参数。这也是为何 hook.callAsync 相较同步钩子的 hook.call,可以多传入一个可执行的回调参数的原因。


    我们接着看 callTap 方法里新增的对 tapAsynctapPromise 订阅回调的模板处理逻辑。

    ⑴ 生成 tapAsync 订阅回调模板

    是最终在 create 方法中,通过 new Function 时传入的形参,代表用户传入 hook.callAsync 的回调参数(最后一个参数,在报错或全部订阅事件结束时候触发):

    时,callTapsSeries 生成的函数片段字符串:

    方式订阅的回调的模板生成方式,我们来看下 hook.tapPromise 是如何生成模板的:

    的能力来决定下一个订阅回调的执行时机:

    示例

    用户调用 asyncSeriesHook.callAsync 时,callTapsSeries 生成的函数片段字符串:

    AsyncSeriesHook 的表现基本一致,不过会判断订阅事件回调的返回值是否为 undefined,如果非 undefined 会中断后续订阅回调的执行:

    的实现我们可以得知,它相较 SyncHook 而言只是新增了一个 onResult 来进一步处理模板逻辑。

    AsyncSeriesBailHook 的实现也是如此,只需要在 AsyncSeriesHook 的基础上添加一个 onResult,对上一个订阅回调返回值进行判断即可:

    时,callTapsSeries 生成的函数片段字符串:

    hook.tapAsync 订阅回调对应模板:

    订阅回调对应模板:

    方法中添加对应逻辑。因此,通过最终的模板来反推功能的实现,也是一种理解 Tapable 源码的方式。

    也是异步串行的钩子,不过在执行时,上一个订阅回调的返回值会传递给下一个订阅回调,并覆盖掉新订阅回调的第一个参数:

    的实现一样,AsyncSeriesWaterfallHook 可以通过修改 onResultonDone 方式来实现:

    时,callTapsSeries 生成的函数片段字符串:

    是一个异步并行的钩子,全部订阅回调都会同时并行触发:

    钩子需要新增 this.callTapsLooping 方法,在 this.callTapsSeries 外头多套一层模板来实现具体需求。

    这次的 AsyncParallelHook 钩子也需要新增一个 this.callTapsParallel 方法实现并行能力,但会摒弃串行的 this.callTapsSeries 接口,改而直接调用 callTap

    的具体实现:

    新增的模板会遍历订阅对象,然后逐个扔给 callTap 生成单个订阅回调的模板,再将它们拼接起来同步执行:

    的“事件终止”回调)。

    示例

    用户调用 asyncSeriesHook.callAsync 时,callTapsParallel 生成的函数片段字符串:

    AsyncParallelHook 基本一致,但如果前一个订阅回调返回了非 undefined 的值,会中断后续其它订阅回调的执行,并触发用户传入 hook.callAsync 的“事件终止”回调:

    的值,但在它返回前,其它并行执行的订阅回调会照常执行不受影响。这种情况唯一受影响的,是“事件终止”回调的执行位置:

    订阅回调结束后触发,因为该订阅回调返回了 true。另外因为该回调是异步的,所以其它的订阅回调会照常被并发执行。

    的实现比较粗暴直接,是在 AsyncParallelBailHook.js 里定义 content 的方法中,在模板前面新增一段内容:

    参数,它可传递给 this.callTapsParallel 来灵活处理 callTap 方法生成的模板:

    方法为模板新增了一个 _results 数组用于存储订阅回调的执行信息(返回值和错误);还新增一个 _checkDone 方法,通过遍历 _results 来检查事件是否应该结束 —— 若发现某个订阅回调执行出错,或者返回了非 undefined 值,_checkDone 方法会返回 true 并执行用户传入的“事件终止”回调)。

    每个订阅回调执行后,会把其执行信息写入 _results 数组并执行 _checkDone()

    示例

    用户调用 asyncParallelBailHook.callAsync 时,content 方法生成的函数片段字符串:

    是 Tapable 不对外暴露的隐藏钩子,但它并不神秘 —— 它和第 5 节所介绍的 SyncLoopHook 的表现一致,订阅回调都是按顺序串行执行的(前一个订阅回调执行完了才会开始执行下一个回调),若有回调返回了非 undefined 的值,会中断进度从头开始整个流程。区别只是在 AsyncSeriesLoopHook 里被执行的订阅回调是异步的:

    模块和 SyncLoopHook 模块基本是一样的,都使用了 this.callTapsLooping 接口来实现串行执行、循环执行的能力:

    接口只能处理同步的订阅回调,为了让其可以处理异步的订阅回调,需要加一点改动:

    函数方便在异步的订阅回调返回了非 undefined 的时候,来递归调用自己实现循环。

    SyncLoopHook 的那套 do-while 只适合同步的订阅回调,因为如果遇上异步的订阅回调,等它执行完毕时 do-while 已经执行结束了,无法再循环。

    示例

    用户调用 asyncSeriesLoopHook.callAsync 时,callTapsLooping 生成的函数片段字符串:

    方法时执行拦截回调。当前钩子有多少个订阅事件就会执行多少次 register 拦截回调,可以在该拦截回调里修改订阅者信息。
  • call:用户调用 hook.call/callAsync 时触发,在订阅事件的回调执行前执行,参数为用户传参。只会触发一次。
  • loop:loop 类型钩子每次循环起始时触发(排在 call 拦截器后面),参数为用户传参。循环几次就会触发几次。
  • tap:调用 hook.call/callAsync 时触发,在订阅事件的回调执行前执行(排在 call 和 loop 拦截器后面),参数为订阅者信息。有多个订阅回调就会执行多次。
  • error:调用 hook.call/callAsync 时触发,拦截时机为执行订阅回调出错时,参数为错误对象。
  • done:调用 hook.call/callAsync 时触发,拦截时机为全部订阅回调执行完毕的时候(排在用户传入的“事件终止”回调前面),没有参数。
  • 拦截器示例 - 同步订阅回调:

    的接口,我们需要回到 Hook.js 中添加该方法,并新增一个数组来存放拦截器配置:

    的数组里获取到拦截器配置,自然也可以在需要拦截的地方,将 this.interceptors[n].interceptorName() 字符串嵌入模板对应位置,最终执行模板函数时,就会在适当的时间点执行对应的拦截回调。

    例如在 HookCodeFactory.js 中,我们可以新增一个 contentWithInterceptors 方法,在调用 this.content 前触发  call 拦截器,并修改传入 this.contentonErroronDone 模板,让它们在执行时分别先触发 error 拦截器和 done 拦截器:

    方法中调用 content 的地方改为 contentWithInterceptors

    开始执行时触发的,它的实现很简单:

    do-while 模板开头插入拦截代码即可:

    是 Tapable 的一个辅助类(helper),利用它可以更好地封装我们的各种钩子:

    使用了 this._factory 来存储用户在初始化时传入的钩子构造函数(钩子工厂),后续用户调用 hookMap.for 时会通过该构造函数生成指定类型的钩子,并以钩子名称为 key 存入一个 Map 对象,后续如果需要获取该钩子,从 Map 对象查找它的名称即可:

    的开头会先判断是否已经构建了该名称的钩子,如果是则直接返回。

    我们就这样完成了 HookMap 的基础能力,可见它就是一个语法糖,实现相对简单。

    另外 HookMap 支持一个名为 factory 的拦截器,它可以修改 HookMap 的钩子构造函数(this._factory),对新建的钩子会生效:

    的模块了,它也是一个语法糖,方便你批量操作多个钩子:

    中,在调用内部方法的时候通过 for of 来遍历钩子和执行对应接口:

    /**** @file HookMap.js ****/
    class MultiHook 
     constructor(hooks, name = undefined) 
      this.hooks = hooks;
      this.name = name;
     

     tap(options, fn) 
      for (const hook of this.hooks) 
       hook.tap(options, fn);
      
     

     tapAsync(options, fn) 
      for (const hook of this.hooks) 
       hook.tapAsync(options, fn);
      
     

     tapPromise(options, fn) 
      for (const hook of this.hooks) 
       hook.tapPromise(options, fn);
      
     

     intercept(interceptor) 
      for (const hook of this.hooks) 
       hook.intercept(interceptor);
      
     


    module.exports = MultiHook;

    十五、小结

    以上就是全部关于 Tapable 的分析了,从中我们了解到了 Tapable 的实现是基于模板的拼接,这是个很有创意的形式,有点像搭积木,把各钩子的订阅回调按相关逻辑一层层搭建成型,这其实不是很轻松的事情。

    在掌握了 Tapable 各种钩子、拦截器的执行流程和实现之后,也相信你会对 Webpack 的工作流程有了更进一步的了解,毕竟 Webpack 的工作流程不外乎就是将各个插件串联起来,而 Tapable 帮忙实现了这一事件流机制。

    另外这么长的文章应该存在一些错别字或语病,欢迎大家评论指出,我再一一修改。

    最后感谢大家能耐心读完本文,希望你们能有所收获,共勉~

    webpack原理:Tapable源码分析及钩子函数作用分析

    webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。

    Webpack 可以认为是一种基于事件流的编程范例,内部的工作流程都是基于 插件 机制串接起来

    而将这些插件粘合起来的就是webpack自己写的基础类 Tapable 是,plugin方法就是该类暴露出来的;

    基于该类规范而其的 Webpack 体系保证了插件的有序性,使得整个系统非常有弹性,扩展性很好;然而有一个致命的缺点就是调试、看源码真是很痛苦,各种跳来跳去;(基于事件流的写法,和程序语言中的 goto 语句很类似)

    在Tapable1.0之前,也就是webpack3及其以前使用的Tapable,提供了包括

    我们可以看到,Tapable就像nodejs中EventEmitter,提供对事件的注册on和触发emit,理解它很重要

    Tapable中的钩子函数

    tapable包暴露出很多钩子类,这些类可以用来为插件创建钩子函数。

    从 https://github.com/webpack/tapable,lib/index.js看出,tapable提供了九种钩子:

    const 
    	SyncHook,
    	SyncBailHook,
    	SyncWaterfallHook,
    	SyncLoopHook,
    	AsyncParallelHook,
    	AsyncParallelBailHook,
    	AsyncSeriesHook,
    	AsyncSeriesBailHook,
    	AsyncSeriesWaterfallHook
      = require("tapable");

    所有钩子类的构造函数都接收一个可选的参数,这个参数是一个由字符串参数组成的数组,如下:

    const hook = new SyncHook(["arg1", "arg2", "arg3"]);

    new Hook 新建钩子

    下面我们就详细介绍一下钩子的用法,以及一些钩子类实现的原理。

    hooks概览

    常用的钩子主要包含以下几种,分为同步和异步,异步又分为并发执行和串行执行,如下图:

    首先,整体感受下钩子的用法,如下

    钩子名称执行方式使用要点
    SyncHook 同步串行 不关心监听函数的返回值
    SyncBailHook 同步串行 只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑
    SyncWaterfallHook 同步串行 上一个监听函数的返回值可以传给下一个监听函数
    SyncLoopHook 同步循环 当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环
    AsyncParallelHook 异步并发 不关心监听函数的返回值
    AsyncParallelBailHook 异步并发 只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数
    AsyncSeriesHook 异步串行 不关系callback()的参数
    AsyncSeriesBailHook 异步串行 callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数
    AsyncSeriesWaterfallHook 异步串行 上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数

    钩子分为同步VS 异步,细分为  并行VS串行,在根据返回值,细分为不同种类。

    Tabable 关键词解析

     

    typefunction
    Hook 所有钩子的后缀
    Waterfall 同步方法,但是它会传值给下一个函数
    Bail 熔断:当函数有任何返回值,就会在当前执行函数停止
    Loop 监听函数返回true表示继续循环,返回undefine表示结束循环
    Sync 同步方法
    AsyncSeries 异步串行钩子
    AsyncParallel 异步并行执行钩子

     

     

    我们可以根据自己的开发需求,选择适合的同步/异步钩子。

    Tapable Hook类

     

    class Hook 
    	constructor(args) 
    		if(!Array.isArray(args)) args = [];
    		this._args = args; // 实例钩子的时候的string类型的数组
    		this.taps = []; // 消费者
    		this.interceptors = []; // interceptors
    		this.call = this._call =  // 以sync类型方式来调用钩子
    		this._createCompileDelegate("call", "sync");
    		this.promise = 
    		this._promise = // 以promise方式
    		this._createCompileDelegate("promise", "promise");
    		this.callAsync = 
    		this._callAsync = // 以async类型方式来调用
    		this._createCompileDelegate("callAsync", "async");
    		this._x = undefined; // 
    	
    
    	_createCall(type) 
    		return this.compile(
    			taps: this.taps,
    			interceptors: this.interceptors,
    			args: this._args,
    			type: type
    		);
    	
    
    	_createCompileDelegate(name, type) 
    		const lazyCompileHook = (...args) => 
    			this[name] = this._createCall(type);
    			return this[name](...args);
    		;
    		return lazyCompileHook;
    	
    	// 调用tap 类型注册
    	tap(options, fn) 
    		// ...
    		options = Object.assign( type: "sync", fn: fn , options);
    		// ...
    		this._insert(options);  // 添加到 this.taps中
    	
    	// 注册 async类型的钩子
    	tapAsync(options, fn) 
    		// ...
    		options = Object.assign( type: "async", fn: fn , options);
    		// ...
    		this._insert(options); // 添加到 this.taps中
    	
    	注册 promise类型钩子
    	tapPromise(options, fn) 
    		// ...
    		options = Object.assign( type: "promise", fn: fn , options);
    		// ...
    		this._insert(options); // 添加到 this.taps中
    	
    	
    

    每次都是调用tap、tapSync、tapPromise注册不同类型的插件钩子,通过调用call、callAsync 、promise方式调用。其实调用的时候为了按照一定的执行策略执行,调用compile方法快速编译出一个方法来执行这些插件。

    tabpack提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。

     

    Async*Sync*
    绑定:tapAsync/tapPromise/tap 绑定:tap
    执行:callAsync/promise 执行:call
    call/callAsync 执行绑定事件

     

    const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
    
    //绑定事件到webapck事件流
    hook1.tap(\'hook1\', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3
    
    //执行绑定的事件
    hook1.call(1,2,3)

    举个栗子

    //引入tapable
    const 
        SyncHook,
        AsyncParallelHook
     = require(\'tapable\');
    
    //创建类
    class Car 
        constructor() 
            this.hooks = 
                accelerate: new SyncHook(["newSpeed"]),
                break: new SyncHook(),
                calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
            ;
        
    
    
    const myCar = new Car();
    
    //绑定同步钩子
    myCar.hooks.break.tap("WarningLampPlugin", () => console.log(\'WarningLampPlugin\'));
    
    //绑定同步钩子 并传参
    myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to $newSpeed`));
    
    //绑定一个异步Promise钩子
    myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => 
        // return a promise
        return new Promise((resolve,reject)=>
            setTimeout(()=>
                console.log(`tapPromise to $source$target$routesList`)
                resolve();
            ,1000)
        )
    );
    
    //执行同步钩子
    myCar.hooks.break.call();
    myCar.hooks.accelerate.call(\'hello\');
    
    console.time(\'cost\');
    
    //执行异步钩子
    myCar.hooks.calculateRoutes.promise(\'i\', \'love\', \'tapable\').then(() => 
        console.timeEnd(\'cost\');
    , err => 
        console.error(err);
        console.timeEnd(\'cost\');
    )

    运行结果

    WarningLampPlugin
    Accelerating to hello
    tapPromise to ilovetapable
    cost: 1003.898ms

    calculateRoutes也可以使用tapAsync绑定钩子,注意:此时用callback结束异步回调。

    myCar.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => 
        // return a promise
        setTimeout(() => 
            console.log(`tapAsync to $source$target$routesList`)
            callback();
        , 2000)
    );
    
    myCar.hooks.calculateRoutes.callAsync(\'i\', \'like\', \'tapable\', err => 
        console.timeEnd(\'cost\');
        if(err) console.log(err)
    )

     

    sync* 钩子

    对于Sync*类型的钩子来说。

    // 所有的钩子都继承于Hook
    class Sync* extends Hook  
    	tapAsync()  // Sync*类型的钩子不支持tapAsync
    		throw new Error("tapAsync is not supported on a Sync*");
    	
    	tapPromise() // Sync*类型的钩子不支持tapPromise
    		throw new Error("tapPromise is not supported on a Sync*");
    	
    	compile(options)  // 编译代码来按照一定的策略执行Plugin
    		factory.setup(this, options);
    		return factory.create(options);
    	
    

     

     

    同步串行

    SyncHook

    不关心监听函数的返回值

    SyncHook的用法及实现
    const  SyncHook  = require("tapable");
    let queue = new SyncHook([\'name\']); //所有的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。
    
    // 订阅-》 注册监听函数
    queue.tap(\'1\', function (name, name2) // tap 的第一个参数是用来标识订阅的函数的
        console.log(name, name2, 1);
        return \'1\'
    );
    queue.tap(\'2\', function (name) 
        console.log(name, 2);
    );
    queue.tap(\'3\', function (name) 
        console.log(name, 3);
    );
    
    // 发布
    queue.call(\'webpack\', \'webpack-cli\');// 发布的时候触发订阅的函数 同时传入参数
    
    // 执行结果:
    /* 
    webpack undefined 1 // 传入的参数需要和new实例的时候保持一致,否则获取不到多传的参数
    webpack 2
    webpack 3
    */

    通过上面如何使用的案例看出,主要是三个步骤(以同步钩子为例)

    1. new SyncHook([\'xx\']) 实例化Hook

    2. hook.tap(\'xxx\', () => ) 注册钩子

    3. hook.call(args) 调用钩子

    原理

    SyncHook是一个很典型的通过发布订阅方式实现的

    class SyncHook_MY
        constructor()
            this.hooks = [];
        
    
        // 订阅
        tap(name, fn)
            this.hooks.push(fn);
        
    
        // 发布
        call()
            this.hooks.forEach(hook => hook(...arguments));
        
    

    SyncBailHook

    只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑

    SyncBailHook的用法及实现

    SyncBailHook为同步串行的执行关系,只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑,用法如下:

    const 
        SyncBailHook
     = require("tapable");
    
    let queue = new SyncBailHook([\'name\']); 
    
    queue.tap(\'1\', function (name) 
        console.log(name, 1);
    );
    queue.tap(\'2\', function (name) 
        console.log(name, 2);
        return \'wrong\'
    );
    queue.tap(\'3\', function (name) 
        console.log(name, 3);
    );
    
    queue.call(\'webpack\');
    
    // 执行结果:
    /* 
    webpack 1
    webpack 2
    */

     

    原理
    // 钩子是同步的,bail -> 保险
    class SyncBailHook 
      // args => ["name"]
      constructor() 
        this.tasks = [];
      
      tap(name, task) 
        this.tasks.push(task);
      
      call(...args) 
        // 当前函数的返回值
        let ret;
        // 当前要先执行第一个
        let index = 0;
        do 
          ret = this.tasks[index++](...args);
         while (ret === undefined && index < this.tasks.length);
      
    

    SyncWaterfallHook的用法及实现

    上一个监听函数的返回值可以传给下一个监听函数

    SyncWaterfallHook为同步串行的执行关系,上一个监听函数的返回值可以传给下一个监听函数,用法如下:

    const 
        SyncBailHook
     = require("tapable");
    
    let queue = new SyncBailHook([\'name\']); 
    
    queue.tap(\'1\', function (name) 
        console.log(name, 1);
    );
    queue.tap(\'2\', function (name) 
        console.log(name, 2);
        return \'wrong\'
    );
    queue.tap(\'3\', function (name) 
        console.log(name, 3);
    );
    
    queue.call(\'webpack\');
    
    // 执行结果:
    /* 
    webpack 1
    webpack 2
    */

     

    SyncWaterfallHook的实现:
    // 钩子是同步的
    class SyncWaterfallHook 
      // args => ["name"]
      constructor() 
        this.tasks = [];
      
      tap(name, task) 
        this.tasks.push(task);
      
      call(...args) 
        let [first, ...others] = this.tasks;
        let ret = first(...args);
        others.reduce((a, b) => 
          return b(a);
        , ret);
      
    
    // 简化版
    class SyncBailHook_MY 
        constructor() 
            this.hooks = [];
        
    
        // 订阅
        tap(name, fn) 
            this.hooks.push(fn);
        
    
        // 发布
        call() 
            for (let i = 0, l = this.hooks.length; i < l; i++) 
                let hook = this.hooks[i];
                let result = hook(...arguments);
                if (result) 
                    break;
                
            
        
    

     

    SyncLoopHook的用法及实现

    当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环

    SyncLoopHook为同步循环的执行关系,当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环,用法如下:

    const 
        SyncWaterfallHook
     = require("tapable");
    
    let queue = new SyncWaterfallHook([\'name\']);
    
    // 上一个函数的返回值可以传给下一个函数
    queue.tap(\'1\', function (name) 
        console.log(name, 1);
        return 1;
    );
    queue.tap(\'2\', function (data) 
        console.log(data, 2);
        return 2;
    );
    queue.tap(\'3\', function (data) 
        console.log(data, 3);
    );
    
    queue.call(\'webpack\');
    
    // 执行结果:
    /* 
    webpack 1
    1 2
    2 3
    */

     

    SyncLoopHook的实现:
    // 钩子是同步的
    class SyncLoopHook 
      // args => ["name"]
      constructor() 
        this.tasks = [];
      
      tap(name, task) 
        this.tasks.push(task);
      
      call(...args) 
        this.tasks.forEach(task => 
          let ret;
          do 
            ret = task(...args);
           while (ret != undefined);
        );
      
    

     

    async* 钩子

    对于Async*类型钩子

    有三种注册/发布的模式,如下:

     

    异步订阅调用方法
    tap callAsync
    tapAsync callAsync
    tapPromise promise

     

    异步并行

    AsyncParallelHook的用法及实现

    不关心监听函数的返回值。

    const 
        AsyncParallelHook
     = require("tapable");
    
    let queue1 = new AsyncParallelHook([\'name\']);
    console.time(\'cost\');
    queue1.tap(\'1\', function (name) 
        console.log(name, 1);
    );
    queue1.tap(\'2\', function (name) 
        console.log(name, 2);
    );
    queue1.tap(\'3\', function (name) 
        console.log(name, 3);
    );
    queue1.callAsync(\'webpack\', err => 
        console.timeEnd(\'cost\');
    );
    
    // 执行结果
    /* 
    webpack 1
    webpack 2
    webpack 3
    cost: 4.520ms
    */

    usage - tapAsync

    let queue2 = new AsyncParallelHook([\'name\']);
    console.time(\'cost1\');
    queue2.tapAsync(\'1\', function (name, cb) 
        setTimeout(() => 
            console.log(name, 1);
            cb();
        , 1000);
    );
    queue2.tapAsync(\'2\', function (name, cb) 
        setTimeout(() => 
            console.log(name, 2);
            cb();
        , 2000);
    );
    queue2.tapAsync(\'3\', function (name, cb) 
        setTimeout(() => 
            console.log(name, 3);
            cb();
        , 3000);
    );
    
    queue2.callAsync(\'webpack\', () => 
        console.log(\'over\');
        console.timeEnd(\'cost1\');
    );
    
    // 执行结果
    /* 
    webpack 1
    webpack 2
    webpack 3
    over
    time: 3004.411ms
    */

    usage - promise

    let queue3 = new AsyncParallelHook([\'name\']);
    console.time(\'cost3\');
    queue3.tapPromise(\'1\', function (name, cb) 
       return new Promise(function (resolve, reject) 
           setTimeout(() => 
               console.log(name, 1);
               resolve();
           , 1000);
       );
    );
    
    queue3.tapPromise(\'1\', function (name, cb) 
       return new Promise(function (resolve, reject) 
           setTimeout(() => 
               console.log(name, 2);
               resolve();
           , 2000);
       );
    );
    
    queue3.tapPromise(\'1\', function (name, cb) 
       return new Promise(function (resolve, reject) 
           setTimeout(() => 
               console.log(name, 3);
               resolve();
           , 3000);
       );
    );
    
    queue3.promise(\'webpack\')
       .then(() => 
           console.log(\'over\');
           console.timeEnd(\'cost3\');
       , () => 
           console.log(\'error\');
           console.timeEnd(\'cost3\');
       );
    /* 
    webpack 1
    webpack 2
    webpack 3
    over
    cost3: 3007.925ms
    */
    AsyncParallelHook的实现:
    class SyncParralleHook   constructor()     this.tasks = [];
        tapAsync(name, task)     this.tasks.push(task);
        callAsync(...args)     // 拿出最终的函数
        let finalCallBack = args.pop();    let index = 0;    // 类似Promise.all
        let done = () => 
          index++;      if (index === this.tasks.length) 
            finalCallBack();
          
        ;    this.tasks.forEach(task => 
          task(...args, done);
        );
      
    

     

    AsyncParallelBailHook

    只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数。

    usage - tap

    let queue1 = new AsyncParallelBailHook([\'name\']);console.time(\'cost\');
    queue1.tap(\'1\', function (name)     console.log(name, 1);
    );
    queue1.tap(\'2\', function (name)     console.log(name, 2);    return \'wrong\');
    queue1.tap(\'3\', function (name)     console.log(name, 3);
    );
    queue1.callAsync(\'webpack\', err =>     console.timeEnd(\'cost\');
    );// 执行结果:/* 
    webpack 1
    webpack 2
    cost: 4.975ms
     */

    usage - tapAsync

    let queue2 = new AsyncParallelBailHook([\'name\']);
    console.time(\'cost1\');
    queue2.tapAsync(\'1\', function (name, cb) 
        setTimeout(() => 
            console.log(name, 1);
            cb();
        , 1000);
    );
    queue2.tapAsync(\'2\', function (name, cb) 
        setTimeout(() => 
            console.log(name, 2);
            return \'wrong\';// 最后的回调就不会调用了
            cb();
        , 2000);
    );
    queue2.tapAsync(\'3\', function (name, cb) 
        setTimeout(() => 
            console.log(name, 3);
            cb();
        , 3000);
    );
    
    queue2.callAsync(\'webpack\', () => 
        console.log(\'over\');
        console.timeEnd(\'cost1\');
    );
    
    // 执行结果:
    /* 
    webpack 1
    webpack 2
    webpack 3
    */

    usage - promise

    let queue3 = new AsyncParallelBailHook([\'name\']);
    console.time(\'cost3\');
    queue3.tapPromise(\'1\', function (name, cb) 
        return new Promise(function (resolve, reject) 
            setTimeout(() => 
                console.log(name, 1);
                resolve();
            , 1000);
        );
    );
    
    queue3.tapPromise(\'2\', function (name, cb) 
        return new Promise(function (resolve, reject) 
            setTimeout(() => 
                console.log(name, 2);
                reject(\'wrong\');// reject()的参数是一个不为null的参数时,最后的回调就不会再调用了
            , 2000);
        );
    );
    
    queue3.tapPromise(\'3\', function (name, cb) 
        return new Promise(function (resolve, reject) 
            setTimeout(() => 
                console.log(name, 3);
                resolve();
            , 3000);
        );
    );
    
    queue3.promise(\'webpack\')
        .then(() => 
            console.log(\'over\');
            console.timeEnd(\'cost3\');
        , () => 
            console.log(\'error\');
            console.timeEnd(\'cost3\');
        );
    
    // 执行结果:
    /* 
    webpack 1
    webpack 2
    error
    cost3: 2009.970ms
    webpack 3
    */
    AsyncSeriesHook的实现:
    class SyncSeriesHook 
      constructor() 
        this.tasks = [];
      
      tapAsync(name, task) 
        this.tasks.push(task);
      
      callAsync(...args) 
        let finalCallback = args.pop();
        let index = 0;
        let next = () => 
          if (this.tasks.length === index) return finalCallback();
          let task = this.tasks[index++];
          task(...args, next);
        ;
        next();
      
    

     

    异步串行

    AsyncSeriesWaterfallHook的用法及实现

    不关系callback()的参数

    AsyncSeriesWaterfallHook为异步串行的执行关系,上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数

    usage - tap

    const 
        AsyncSeriesHook
     = require("tapable");
    
    // tap
    let queue1 = new AsyncSeriesHook([\'name\']);
    console.time(\'cost1\');
    queue1.tap(\'1\', function (name) 
        console.log(1);
        return "Wrong";
    );
    queue1.tap(\'2\', function (name) 
        console.log(2);
    );
    queue1.tap(\'3\', function (name) 
        console.log(3);
    );
    queue1.callAsync(\'zfpx\', err => 
        console.log(err);
        console.timeEnd(\'cost1\');
    );
    // 执行结果
    /* 
    1
    2
    3
    undefined
    cost1: 3.933ms
    */

    usage - tapAsync

    let queue2 = new AsyncSeriesHook([\'name\']);
    console.time(\'cost2\');
    queue2.tapAsync(\'1\', function (name, cb) 
        setTimeout(() => 
            console.log(name, 1);
            cb();
        , 1000);
    );
    queue2.tapAsync(\'2\', function (name, cb) 
        setTimeout(() => 
            console.log(name, 2);
            cb();
        , 2000);
    );
    queue2.tapAsync(\'3\', function (name, cb) 
        setTimeout(() => 
            console.log(name, 3);
            cb();
        , 3000);
    );
    
    queue2.callAsync(\'webpack\', (err) => 
        console.log(err);
        console.log(\'over\');
        console.timeEnd(\'cost2\');
    ); 
    // 执行结果
    /* 
    webpack 1
    webpack 2
    webpack 3
    undefined
    over
    cost2: 6019.621ms
    */

    usage - promise

    let queue3 = new AsyncSeriesHook([\'name\']);
    console.time(\'cost3\');
    queue3.tapPromise(\'1\',function(name)
       return new Promise(function(resolve)
           setTimeout(function()
               console.log(name, 1);
               resolve();
           ,1000)
       );
    );
    queue3.tapPromise(\'2\',function(name,callback)
        return new Promise(function(resolve)
            setTimeout(function()
                console.log(name, 2);
                resolve();
            ,2000)
        );
    );
    queue3.tapPromise(\'3\',function(name,callback)
        return new Promise(function(resolve)
            setTimeout(function()
                console.log(name, 3);
                resolve();
            ,3000)
        );
    );
    queue3.promise(\'webapck\').then(err=>
        console.log(err);
        console.timeEnd(\'cost3\');
    );
    
    // 执行结果
    /* 
    webapck 1
    webapck 2
    webapck 3
    undefined
    cost3: 6021.817ms
    */

    原理

    class AsyncSeriesHook_MY 
        constructor() 
            this.hooks = [];
        
    
        tapAsync(name, fn) 
            this.hooks.push(fn);
        
    
        callAsync() 
            var slef = this;
            var args = Array.from(arguments);
            let done = args.pop();
            let idx = 0;
    
            function next(err) 
                // 如果next的参数有值,就直接跳跃到 执行callAsync的回调函数
                if (err) return done(err);
                let fn = slef.hooks[idx++];
                fn ? fn(...args, next) : done();
            
            next();
        
    

    AsyncSeriesBailHook

    callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数

    usage - tap

    const 
        AsyncSeriesBailHook
     = require("tapable");
    
    // tap
    let queue1 = new AsyncSeriesBailHook([\'name\']);
    console.time(\'cost1\');
    queue1.tap(\'1\', function (name) 
        console.log(1);
        return "Wrong";
    );
    queue1.tap(\'2\', function (name) 
        console.log(2);
    );
    queue1.tap(\'3\', function (name) 
        console.log(3);
    );
    queue1.callAsync(\'webpack\', err => 
        console.log(err);
        console.timeEnd(\'cost1\');
    );
    
    // 执行结果:
    /* 
    1
    null
    cost1: 3.979ms
    */

    usage - tapAsync

    let queue2 = new AsyncSeriesBailHook([\'name\']);
    console.time(\'cost2\');
    queue2.tapAsync(\'1\', function (name, callback) 
        setTimeout(function () 
            console.log(name, 1);
            callback();
        , 1000)
    );
    queue2.tapAsync(\'2\', function (name, callback) 
        setTimeout(function () 
            console.log(name, 2);
            callback(\'wrong\');
        , 2000)
    );
    queue2.tapAsync(\'3\', function (name, callback) 
        setTimeout(function () 
            console.log(name, 3);
            callback();
        , 3000)
    );
    queue2.callAsync(\'webpack\', err => 
        console.log(err);
        console.log(\'over\');
        console.timeEnd(\'cost2\');
    );
    // 执行结果
    
    /* 
    webpack 1
    webpack 2
    wrong
    over
    cost2: 3014.616ms
    */

    usage - promise

    let queue3 = new AsyncSeriesBailHook([\'name\']);
    console.time(\'cost3\');
    queue3.tapPromise(\'1\', function (name) 
        return new Promise(function (resolve, reject) 
            setTimeout(function () 
                console.log(name, 1);
                resolve();
            , 1000)
        );
    );
    queue3.tapPromise(\'2\', function (name, callback) 
        return new Promise(function (resolve, reject) 
            setTimeout(function () 
                console.log(name, 2);
                reject();
            , 2000)
        );
    );
    queue3.tapPromise(\'3\', function (name, callback) 
        return new Promise(function (resolve) 
            setTimeout(function () 
                console.log(name, 3);
                resolve();
            , 3000)
        );
    );
    queue3.promise(\'webpack\').then(err => 
        console.log(err);
        console.log(\'over\');
        console.timeEnd(\'cost3\');
    , err => 
        console.log(err);
        console.log(\'error\');
        console.timeEnd(\'cost3\');
    );
    // 执行结果:
    /* 
    webpack 1
    webpack 2
    undefined
    error
    cost3: 3017.608ms
    */

    AsyncSeriesWaterfallHook

    上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数

    usage - tap

    const 
        AsyncSeriesWaterfallHook
     = require("tapable");
    
    // tap
    let queue1 = new AsyncSeriesWaterfallHook([\'name\']);
    console.time(\'cost1\');
    queue1.tap(\'1\', function (name) 
        console.log(name, 1);
        return \'lily\'
    );
    queue1.tap(\'2\', function (data) 
        console.log(2, data);
        return \'Tom\';
    );
    queue1.tap(\'3\', function (data) 
        console.log(3, data);
    );
    queue1.callAsync(\'webpack\', err => 
        console.log(err);
        console.log(\'over\');
        console.timeEnd(\'cost1\');
    );
    
    // 执行结果:
    /* 
    webpack 1
    2 \'lily\'
    3 \'Tom\'
    null
    over
    cost1: 5.525ms
    */

    usage - tapAsync

    let queue2 = new AsyncSeriesWaterfallHook([\'name\']);
    console.time(\'cost2\');
    queue2.tapAsync(\'1\', function (name, callback) 
        setTimeout(function () 
            console.log(\'1: \', name);
            callback(null, 2);
        , 1000)
    );
    queue2.tapAsync(\'2\', function (data, callback) 
        setTimeout(function () 
            console.log(\'2: \', data);
            callback(null, 3);
        , 2000)
    );
    queue2.tapAsync(\'3\', function (data, callback) 
        setTimeout(function () 
            console.log(\'3: \', data);
            callback(null, 3);
        , 3000)
    );
    queue2.callAsync(\'webpack\', err => 
        console.log(err);
        console.log(\'over\');
        console.timeEnd(\'cost2\');
    );
    // 执行结果:
    /* 
    1:  webpack
    2:  2
    3:  3
    null
    over
    cost2: 6016.889ms
    */

    usage - promise

    let queue3 = new AsyncSeriesWaterfallHook([\'name\']);
    console.time(\'cost3\');
    queue3.tapPromise(\'1\', function (name) 
        return new Promise(function (resolve, reject) 
            setTimeout(function () 
                console.log(\'1:\', name);
                resolve(\'1\');
            , 1000)
        );
    );
    queue3.tapPromise(\'2\', function (data, callback) 
        return new Promise(function (resolve) 
            setTimeout(function () 
                console.log(\'2:\', data);
                resolve(\'2\');
            , 2000)
        );
    );
    queue3.tapPromise(\'3\', function (data, callback) 
        return new Promise(function (resolve) 
            setTimeout(function () 
                console.log(\'3:\', data);
                resolve(\'over\');
            , 3000)
        );
    );
    queue3.promise(\'webpack\').then(err => 
        console.log(err);
        console.timeEnd(\'cost3\');
    , err => 
        console.log(err);
        console.timeEnd(\'cost3\');
    );
    // 执行结果:
    /* 
    1: webpack
    2: 1
    3: 2
    over
    cost3: 6016.703ms
    */

    原理

    class AsyncSeriesWaterfallHook_MY 
        constructor() 
            this.hooks = [];
        
    
        tapAsync(name, fn) 
            this.hooks.push(fn);
        
    
        callAsync() 
            let self = this;
            var args = Array.from(arguments);
    
            let done = args.pop();
            console.log(args);
            let idx = 0;
            let result = null;
    
            function next(err, data) 
                if (idx >= self.hooks.length) return done();
                if (err) 
                    return done(err);
                
                let fn = self.hooks[idx++];
                if (idx == 1) 
    
                    fn(...args, next);
                 else 
                    fn(data, next);
                
            
            next();
        
    

     

     

    参考文章:

    webpack插件机制之Tapable https://juejin.cn/post/6844903774645911566

    干货!撸一个webpack插件(内含tapable详解+webpack流程) https://juejin.cn/post/6844903713312604173

    webpack详解 https://juejin.cn/post/6844903573675835400

    webpack4.0源码分析之Tapable https://juejin.cn/post/6844903588112629767

    Webpack 源码(一)—— Tapable 和 事件流 https://segmentfault.com/a/1190000008060440

     

     

     

     


    转载本站文章《webpack原理(3):Tapable源码分析及钩子函数作用分析》,
    请注明出处:https://www.zhoulujun.cn/html/tools/Bundler/webpackTheory/8754.html

    以上是关于硬核解析 Webpack 事件流核心!的主要内容,如果未能解决你的问题,请参考以下文章

    前端工程化-插件机制

    前端工程化-插件机制

    硬核解析,巧用案例学习jQuery框架三种事件绑定方式

    硬核解析,巧用案例学习jQuery框架三种事件绑定方式

    手写webpack增加plugin

    .26-浅析webpack源码之事件流make