硬核解析 Webpack 事件流核心!
Posted 全栈修仙之路
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了硬核解析 Webpack 事件流核心!相关的知识,希望对你有一定的参考价值。
大佬,他会逐步介绍每个钩子,并分析其源码实现。是 Tapable 所提供的最简单的钩子,它是一个同步钩子。本小节的内容会比较长,因为会介绍到很多钩子们共用的方法。学习完 SyncHook
钩子的实现,再去分析其它钩子的源码会轻松很多。
初始化 SyncHook
后,可以通过调用实例的 tap
方法来注册事件,调用 call
方法按注册顺序来执行回调:
在英文中有“窃听”的意思,用它作为订阅事件的方法名,还挺形象和俏皮。模块的源码(简略版)如下:实例化后其实就是一个 Hook
类的实例对象,并带上了一个自定义的 compile
方法。顾名思义,可以猜测 compile
方法是最终调用 call
时所执行的接口。我们先不分析 compile
的实现,就带着对它的猜想,来看看 Hook
类的实现。
钩子相关的代码。这里需要知道的是,其中带下划线的方法如 _tap
、_resetCompilation
、_insert
等都属于各钩子公用的内部方法,而像 tap
、call
方法是 SyncHook
钩子专有的方法。我们综合梳理一下 SyncHook
调用 tap
和 call
的逻辑。
⑴ 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
实例的 setup
和 create
方法,它们都是从父类 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 次 3 onDone
,即 ()=>""
() => "_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.content
和 this.callTapsSeries
的地方分别做点修改,让它们利用这个新增的模板参数,来实现 SyncBailHook
的功能。
⑴ content 调用处的改动
调用 content
时不传 onResult
的简易处理:中最关键的实现,是利用了 current
的变化和传递,来实现各订阅事件回调执行代码字符串的拼接。对于 SyncBailHook
的需求,我们可以利用 onResult
把 current()
的模板包起来(把 onResult
的第三个参数设为 current
即可):
时,我们都能获得如下模板:时,callTapsSeries
生成的函数片段字符串:依旧为同步钩子,不过它会把前一个订阅回调所返回的内容,作为第一个参数传递给后续的订阅回调:的模板参数,来生成 hook.call
最终调用的函数代码。SyncWaterfallHook
的实现也非常简单,只需要调整 onDone
和 onResult
两个模板参数即可:
为订阅对象数组遍历时的初始化模板函数,执行后会生成 return $this._args[0];\\n
字符串。onResult
为订阅对象数组遍历时的非初始化模板函数,会判断上一个订阅回调返回值是否非 undefined
,是则将 syncWaterfallHook.call
的第一个参数改为此返回值,再拼接上一次遍历生成的模板内容。
示例
每次用户调用 syncWaterfallHook.call
时,callTapsSeries
生成的函数片段字符串:
表示如果存在某个订阅事件回调返回了非 undefined
的值,则全部订阅事件回调从头执行:“从头执行全部回调”的逻辑比较特殊,旧的方法已经无法满足该需求,所以 Tapable 为其新开了一个 callTapsLooping
方法来处理:方法的实现:在模板的外层包了个 do while
循环:生成的模板,只需要判断订阅回调返回值是否为 undefined
,然后修改 _loop
即可。而 callTapsLooping
传入 callTapsSeries
的 onResult
参数完善了此块逻辑:
示例
每次用户调用 syncLoopHook.call
时,callTapsLooping
生成的函数片段字符串:
表示一个异步串行的钩子,可以通过 hook.tapAsync
或 hook.tapPromise
方法,来注册异步的事件回调。这些订阅事件的回调依旧是逐个执行,即必须等到上一个异步回调通知钩子它已经执行完毕了,才能开始下一个异步回调:
来订阅事件的异步回调,可以通过执行最后一个参数来通知钩子“我已经执行完毕,可以接着执行后面的回调了”;对于使用 hook.tapPromise
来订阅事件的异步回调,需要返回一个 Promise
,当其状态为 resolve
时,钩子才会开始执行后续其它订阅回调。
另外需要留意下,AsyncSeriesHook
钩子使用新的 hook.callAsync
来执行订阅回调(而不再是 hook.call
),且支持传入回调(最后一个参数),在全部订阅事件执行完毕后触发。
模块的代码,结构和 SyncHook
是基本一样的:方法来处理异步回调中的错误。AsyncSeriesHook
钩子实现的关键点,是对其几个专用方法 hook.tapAsync
、hook.tapPromise
和 hook.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
方法里新增的对 tapAsync
和 tapPromise
订阅回调的模板处理逻辑。
⑴ 生成 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
可以通过修改 onResult
和 onDone
方式来实现:时,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 拦截回调,可以在该拦截回调里修改订阅者信息。
本小节的内容会比较长,因为会介绍到很多钩子们共用的方法。学习完 SyncHook
钩子的实现,再去分析其它钩子的源码会轻松很多。
SyncHook
后,可以通过调用实例的 tap
方法来注册事件,调用 call
方法按注册顺序来执行回调:实例化后其实就是一个 Hook
类的实例对象,并带上了一个自定义的 compile
方法。顾名思义,可以猜测 compile
方法是最终调用 call
时所执行的接口。我们先不分析 compile
的实现,就带着对它的猜想,来看看 Hook
类的实现。
钩子相关的代码。这里需要知道的是,其中带下划线的方法如 _tap
、_resetCompilation
、_insert
等都属于各钩子公用的内部方法,而像 tap
、call
方法是 SyncHook
钩子专有的方法。我们综合梳理一下 SyncHook
调用 tap
和 call
的逻辑。
⑴ 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
实例的 setup
和 create
方法,它们都是从父类 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 次 3 onDone
,即 ()=>""
() => "_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.content
和 this.callTapsSeries
的地方分别做点修改,让它们利用这个新增的模板参数,来实现 SyncBailHook
的功能。
⑴ content 调用处的改动
调用 content
时不传 onResult
的简易处理:中最关键的实现,是利用了 current
的变化和传递,来实现各订阅事件回调执行代码字符串的拼接。对于 SyncBailHook
的需求,我们可以利用 onResult
把 current()
的模板包起来(把 onResult
的第三个参数设为 current
即可):
时,我们都能获得如下模板:时,callTapsSeries
生成的函数片段字符串:依旧为同步钩子,不过它会把前一个订阅回调所返回的内容,作为第一个参数传递给后续的订阅回调:的模板参数,来生成 hook.call
最终调用的函数代码。SyncWaterfallHook
的实现也非常简单,只需要调整 onDone
和 onResult
两个模板参数即可:
为订阅对象数组遍历时的初始化模板函数,执行后会生成 return $this._args[0];\\n
字符串。onResult
为订阅对象数组遍历时的非初始化模板函数,会判断上一个订阅回调返回值是否非 undefined
,是则将 syncWaterfallHook.call
的第一个参数改为此返回值,再拼接上一次遍历生成的模板内容。
示例
每次用户调用 syncWaterfallHook.call
时,callTapsSeries
生成的函数片段字符串:
表示如果存在某个订阅事件回调返回了非 undefined
的值,则全部订阅事件回调从头执行:“从头执行全部回调”的逻辑比较特殊,旧的方法已经无法满足该需求,所以 Tapable 为其新开了一个 callTapsLooping
方法来处理:方法的实现:在模板的外层包了个 do while
循环:生成的模板,只需要判断订阅回调返回值是否为 undefined
,然后修改 _loop
即可。而 callTapsLooping
传入 callTapsSeries
的 onResult
参数完善了此块逻辑:
示例
每次用户调用 syncLoopHook.call
时,callTapsLooping
生成的函数片段字符串:
表示一个异步串行的钩子,可以通过 hook.tapAsync
或 hook.tapPromise
方法,来注册异步的事件回调。这些订阅事件的回调依旧是逐个执行,即必须等到上一个异步回调通知钩子它已经执行完毕了,才能开始下一个异步回调:
来订阅事件的异步回调,可以通过执行最后一个参数来通知钩子“我已经执行完毕,可以接着执行后面的回调了”;对于使用 hook.tapPromise
来订阅事件的异步回调,需要返回一个 Promise
,当其状态为 resolve
时,钩子才会开始执行后续其它订阅回调。
另外需要留意下,AsyncSeriesHook
钩子使用新的 hook.callAsync
来执行订阅回调(而不再是 hook.call
),且支持传入回调(最后一个参数),在全部订阅事件执行完毕后触发。
模块的代码,结构和 SyncHook
是基本一样的:方法来处理异步回调中的错误。AsyncSeriesHook
钩子实现的关键点,是对其几个专用方法 hook.tapAsync
、hook.tapPromise
和 hook.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
方法里新增的对 tapAsync
和 tapPromise
订阅回调的模板处理逻辑。
⑴ 生成 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
可以通过修改 onResult
和 onDone
方式来实现:时,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 拦截回调,可以在该拦截回调里修改订阅者信息。
hook.call/callAsync
时触发,在订阅事件的回调执行前执行,参数为用户传参。只会触发一次。hook.call/callAsync
时触发,在订阅事件的回调执行前执行(排在 call 和 loop 拦截器后面),参数为订阅者信息。有多个订阅回调就会执行多次。hook.call/callAsync
时触发,拦截时机为执行订阅回调出错时,参数为错误对象。hook.call/callAsync
时触发,拦截时机为全部订阅回调执行完毕的时候(排在用户传入的“事件终止”回调前面),没有参数。拦截器示例 - 同步订阅回调:
的接口,我们需要回到 Hook.js
中添加该方法,并新增一个数组来存放拦截器配置:的数组里获取到拦截器配置,自然也可以在需要拦截的地方,将 this.interceptors[n].interceptorName()
字符串嵌入模板对应位置,最终执行模板函数时,就会在适当的时间点执行对应的拦截回调。例如在 HookCodeFactory.js
中,我们可以新增一个 contentWithInterceptors
方法,在调用 this.content
前触发 call 拦截器,并修改传入 this.content
的 onError
和 onDone
模板,让它们在执行时分别先触发 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,提供了包括
-
plugin(name:string, handler:function)注册插件到Tapable对象中
-
apply(…pluginInstances: (AnyPlugin|function)[])调用插件的定义,将事件监听器注册到Tapable实例注册表中
-
applyPlugins*(name:string, …)多种策略细致地控制事件的触发,包括applyPluginsAsync、applyPluginsParallel等方法实现对事件触发的控制,实现
-
多个事件连续顺序执行
-
并行执行
-
异步执行
-
一个接一个地执行插件,前面的输出是后一个插件的输入的瀑布流执行顺序
-
在允许时停止执行插件,即某个插件返回了一个undefined的值,即退出执行
我们可以看到,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 新建钩子
-
tapable 暴露出来的都是类方法,new 一个类方法获得我们需要的钩子。
-
class 接受数组参数options,非必传。类方法会根据传参,接受同样数量的参数。
下面我们就详细介绍一下钩子的用法,以及一些钩子类实现的原理。
hooks概览
常用的钩子主要包含以下几种,分为同步和异步,异步又分为并发执行和串行执行,如下图:
首先,整体感受下钩子的用法,如下
钩子名称 执行方式 使用要点
SyncHook
同步串行
不关心监听函数的返回值
SyncBailHook
同步串行
只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑
SyncWaterfallHook
同步串行
上一个监听函数的返回值可以传给下一个监听函数
SyncLoopHook
同步循环
当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环
AsyncParallelHook
异步并发
不关心监听函数的返回值
AsyncParallelBailHook
异步并发
只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数
AsyncSeriesHook
异步串行
不关系callback()的参数
AsyncSeriesBailHook
异步串行
callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数
AsyncSeriesWaterfallHook
异步串行
上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数
钩子分为同步VS 异步,细分为 并行VS串行,在根据返回值,细分为不同种类。
-
BasicHook: 执行每一个,不关心函数的返回值,有 SyncHook、AsyncParallelHook、AsyncSeriesHook。
-
BailHook: 顺序执行 Hook,遇到第一个结果 result !== undefined 则返回,不再继续执行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。
-
WaterfallHook: 类似于 reduce,如果前一个 Hook 函数的结果 result !== undefined,则 result 会作为后一个 Hook 函数的第一个参数。既然是顺序执行,那么就只有 Sync 和 AsyncSeries 类中提供这个Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook
-
LoopHook: 不停的循环执行 Hook,直到所有函数结果 result === undefined。同样的,由于对串行性有依赖,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暂时没看到具体使用 Case)
Tabable 关键词解析
type function
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)
举个栗子
-
定义一个Car方法,在内部hooks上新建钩子。分别是同步钩子 accelerate、break(accelerate接受一个参数)、异步钩子calculateRoutes
-
使用钩子对应的绑定和执行方法
-
calculateRoutes使用tapPromise可以返回一个promise对象。
//引入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*类型的钩子来说。
-
注册在该钩子下面的插件的执行顺序都是顺序执行。
-
只能使用tap注册,不能使用tapPromise和tapAsync注册
// 所有的钩子都继承于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
*/
通过上面如何使用的案例看出,主要是三个步骤(以同步钩子为例)
-
new SyncHook([\'xx\']) 实例化Hook
-
hook.tap(\'xxx\', () => ) 注册钩子
-
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、tapPromise、tapAsync注册
有三种注册/发布的模式,如下:
异步订阅 调用方法
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 事件流核心!的主要内容,如果未能解决你的问题,请参考以下文章