2. webpack构建的基石: tapable@1.1.3源码分析

2022-11-16 17:26:15 浏览数 (1)

通过一个demo带你深入进入webpack@4.46.0源码的世界,分析构建原理,专栏地址,共有十篇。

  • 1. 从构建前后产物对比分析webpack做了些什么?
  • 2. webpack构建的基石: tapable@1.1.3源码分析
  • 3. webpack构建整体流程的组织:webpack -> Compiler -> Compilation
  • 4. 创建模块实例,为模块解析准备
  • 5. 路径解析:enhanced-resolve@4.5.0源码分析
  • 6. 模块构建之loader执行:loader-runner@2.4.0源码分析
  • 7. 模块构建之解析_source获取dependencies
  • 8. 从dependency graph 到 chunk graph
  • 9. 从chunk到最终的文件内容到最后的文件输出?
  • 10. webpack中涉及了哪些设计模式呢?

如果你看过webpack内部的几个核心类如Compiler、Compilation等对象,会发现有大量的钩子this.hooks = {...},这些hooks让开发者可以高度参与整个构建流程,大大的提供了构建的可扩展性。这个能力是由tapable提供的。

tapable是webpack中插件能运行的基石,是webpack与开发者交流的话筒,增强了webpack基础功能。

tapable是什么

介绍tapable之前,先说下发布-订阅,关于发布订阅,维基百科的解释如下:

在软件架构中,发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

上图是发布订阅模式的原理图,会引入一个中间人;订阅者向该中间人订阅事件,发布者通过中间人发布事件,中间人存在的主要目的是为了解耦发布者和订阅者,二者只需要持有中间人的引用即可。

一个简单的发布订阅的实现和测试用来说明发布订阅的一些特点

easy pub-sub pattern

通常在发布订阅模式中(如EventEmitter3),存在以下问题:

  • 订阅函数是按照订阅的顺序顺序执行并且每个订阅函数都会被执行,执行流程不可中断
  • 订阅函数是异步时不会等待该异步任务完成以后再执行后面的订阅的函数
  • 另外订阅函数之间没有逻辑关系连接,这也是导致第一点执行流程不可中断的原因
  • 发布者拿不到订阅函数的最终执行结果

但是实际业务中可能会有一些更复杂的场景,比如需要订阅函数支持异步并且异步函数的执行是严格按照顺讯执行的,上一个异步函数状态完成后才能进入下一个异步函数的执行流程中,即保证订阅的函数严格串行执行;又比如订阅的多个函数之间可能只需要其中一个满足发布者的条件则整个流程可以中断,有点像策略模式的感觉(掘金有js设计模式的小册,有提到策略模式,可以看下)从第一个策略开始直到命中一个策略,那么后面的策略也不会执行。

那么此时发布订阅就满足不了复杂场景的要求,而webpack在构建场景是比较复杂的,因此自研的tapble来提供增强版的发布订阅来支持复杂的构建场景。

所以:tapable是一种基于发布-订阅的消息范式,但是由于webpack构建场景比较复杂,因此相较于普通版的发布订阅类库其提供了很多增强特性。


下面通过介绍tapble的具体使用案例来直观的感受一下其提供了哪些增强能力。tapable 提供多种hook,每个hook提供的能力都不一样。 tapable@1.1.3

代码语言:javascript复制
// 同步的
SyncHook, 
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
// 异步并行
AsyncParallelHook,
AsyncParallelBailHook,
// 异步串行
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook,
AsyncSeriesLoopHook

这些hook名称中可以看出其具有哪些特性,我们将这些关键词提取出来并分类

  • SyncAsync:订阅的函数是同步的还是异步的,这里的异步支持两种形式:callback 、promise
  • ParallelSeries:多个订阅函数是并行执行还是串行执行,当然同步函数没有并行这一说,所以有AsyncParallelXxxHook而没有SyncParallelXxxHook
  • BasicBailWaterfallLoop:根据每个订阅函数执行完成返回的结果进行一些判断或者添加一些逻辑后再选择进入下一个订阅函数的执行或退出

tapbale中提供的hook有意思的地方在于当我在调用的时候才会通过new Function去动态生成执行代码。我们通过一些案例来研究下各特性之间的区别和联系,以发现这个特性在源码中是如何处理的以及为什么这么处理。


具体看各特性之前先简单介绍下整体的类组织关系以及各类的作用:

  • 两个大类:hook类和codeFactory,其中hook类提供注册和触发的能力,codeFactory类用来动态生成执行代码;具体的xxxHook继承Hook,每个具体的XxxHook都会有与之关联的XxxCodeFactory
  • hook类中的tapXxx用来注册订阅函数,callXxxpromise用来发布事件(触发订阅函数的执行),发布方法实际会调用_createCall而后调用子类(继承Hook)的compile方法来生成匿名函数,compile方法持有该hook对应的codeFactory(具体子类的差异化逻辑交个子类的compile方法),而后调用该codeFactory父类HookCodeFactroy中create来生成代码,差异化处理交给了子类的content方法

下面分别按照大的特性分类来分析。

1. Sync | Async

首先订阅函数可以是同步函数也可以是异步,并且异步函数支持callback和promise两种形式;

SyncXxxHook

用法:使用tap方法来进行订阅,通过call方法来触发事件

代码语言:javascript复制
const synHook = new SyncHook(['arg']); // call时需要传递两个参数
// 订阅函数是同步函数,如果是异步函数,则只会同步执行
synHook.tap('test', (arg) => console.log('1', arg) )
synHook.tap('test', (arg) => console.log('2', arg) )
// 触发订阅函数执行
synHook.call('parameter');

调用call方法时动态生成的执行代码如下

每个订阅函数都会生成同步执行代码顺序执行,同步代码的状态直接取决于这个同步函数的执行过程是否出错,如果没有出错直接进入下一个订阅函数的执行。如果出错但没有捕获则执行过程中断。

每个订阅函数生成代码逻辑(几乎)完全一致,在添加某些其他特性下有些许差别,具体差别后面会再说。

AsyncXxxHook: callback | promise

callback形式的异步订阅函数

代码语言:javascript复制
// callback形式的异步订阅函数: 用法:tapAsync(订阅) - callAsync(触发) 
const asyncSeriesHook = new AsyncSeriesHook(['arg']);
// 订阅函数需要一个接受一个回调,将当前订阅函数的执行结果返回给执执行流
asyncSeriesHook.tapAsync('test', (arg, callback) => setTimeout(() => { callback(null, 1) }, 1000))
asyncSeriesHook.tapAsync('test', (arg, callback) => setTimeout(() => { callback(null, 2) }, 1000))
// 关键,提供一个回调,当所有的订阅函数执行完成后(整个流程结束后)来感知流程是否结束(包括接收最终的结果,异常判断等)
asyncSeriesHook.callAsync('arg-test',(err, result) => console.log('执行结束'))

promise形式的异步订阅函数 `javascript // promise形式的异步订阅函数:tapPromise(订阅) - promise(触发): const asyncSeriesHookPromise = new AsyncSeriesHook('arg'); // 订阅函数需要返回一个promise asyncSeriesHookPromise.tapPromise('test', () => new Promise(resolve => setTimeout(() => { resolve(1) }, 1000))) asyncSeriesHookPromise.tapPromise('test', () => new Promise(resolve => setTimeout(() => { resolve(2) }, 1000)))

// 返回一个promise来接受整个执行流的最终状态 const p = asyncSeriesHookPromise.promise();

const resolveHandler = () => { console.log('resolved') }; const rejectHandler = () => { console.log('rejected') } p.then(resolveHandler, rejectHandler)

代码语言:javascript复制
callAsync() & promise() 动态生成的代码如下:

![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cbc64f995e9e4af3807c4f5373d9f08d~tplv-k3u1fbpfcp-watermark.image?)


<table>
    <thead>
    <tr>
        <th>异同点函数形式</th>
        <th>callback</th>
        <th>promise</th>
    </tr>
    </thead>
    <tbody>
    <tr>
        <td>差异点</td>
        <td>每个订阅函数都会生成callback形式的执行代码即生成的代码中会传入一个回调函数给订阅函数,然后在订阅函数中来执行这个回调,通过回调实现异步状态的流转。所以callback形式的异步的关键在于生成的代码中会提供
            一个回调函数给到订阅函数。
        </td>
        <td>
            每个订阅函数都会返回一个promise,生成的代码中通过该promise来实现异步状态流转决定进入下一个订阅函数的执行还是抛出异常。
        </td>
    </tr>
    <tr>
        <td>相同点</td>
        <td colspan=2>看到每一个订阅函数生成的主代码几乎完全一致。 差别在于
            _fn0不是最后一个订阅函数,因此其执行完成并且没有出错的情况下执行_next()即进入下一个订阅函数的执行。
            而_fn1是最后一个订阅函数,其执行完成后直接调用发布者传递的回调(callAsync传递的函数)或者直接resolve()来结束整个执行流。
        </td>
    </tr>
    </tbody>
</table>

## 小结
其实可以可以看到,每一个订阅函数都会生成各自的代码片段,核心就是为了支持自身状态的正确流转,比如成功则进入下一个订阅函数的执行,出错则结束流程。

这里涉及到两个部分:最终生成的匿名函数的`整体结构`和`单个订阅函数生成的执行代码`

### 整体结构
其中由于整体结构一致,统一收敛到抽象类`HookCodeFactory.create`方法中,当然也会区分`sync`、`async`、`promise`类型,比如`async`(callback形式的异步)形式会给生成的匿名函数添加一个callback参数用来接收发布者传递进来的回调,`promise`(promise形式的异步)需要在外围添加new Promise()返回给发布者。

```javascript

// HookCodeFactory.js

create(options) {

this.init(options);

let fn;

switch (this.options.type) {

代码语言:txt复制
  case "sync":
代码语言:txt复制
     fn = new Function(...);
代码语言:txt复制
     break;
代码语言:txt复制
  case "async":
代码语言:txt复制
     fn = new Function(...);
代码语言:txt复制
     break;
代码语言:txt复制
  case "promise":
代码语言:txt复制
     //...
代码语言:txt复制
     fn = new Function(this.args(), code);
代码语言:txt复制
     break;

}

//...

return fn;

}

代码语言:txt复制
看到通过switch-case来区分订阅函数类型`整体结构`的`模板`,如下表提供了`模板`与上述案例`生成代码`的对照关系:

| 类型结果 | 整体结构(模板) | (上述)demo生成代码 |
|:----|:----|:----|
| sync | ​ | ​ |
| async | ​ | ​ |
| promise | ​ | ​ |


通过this._args()生成匿名函数形参列表;this.header()来获取头部公共部分逻辑比如`var _x =  this._x`用来保存订阅函数列表;this.content()来实现差异化的处理,交给具体的子类实现,以SyncHook为例,如下。

```javascript

class SyncHookCodeFactory extends HookCodeFactory { // 继承父类

代码语言:txt复制
// content实现差异化的关键,由子类实现

content({ onError, onDone, rethrowIfPossible }) {

代码语言:txt复制
   // callTapsSeries内部会调用HookCodeFactory.callTap生成单个订阅函数执行代码
代码语言:txt复制
  return this.callTapsSeries({ 
代码语言:txt复制
     onError: (i, err) => onError(err),
代码语言:txt复制
     onDone,
代码语言:txt复制
     rethrowIfPossible
代码语言:txt复制
  });

}

}

const factory = new SyncHookCodeFactory();

class SyncHook extends Hook {

//...

// 当调用 call、callAsync、promise时,实际会调用父类Hook.lazyCompileHook

// 第一步:调用下面的compile方法生成一个匿名函数代码

//(调用XxxCodeFactory.create,create定义在父类HookCodeFactory中,

// create会再调用子类content方法实现差异化处理)

// 第二步:执行生成的匿名函数

compile(options) {

代码语言:txt复制
  factory.setup(this, options);
代码语言:txt复制
  return factory.create(options);

}

}

代码语言:txt复制
### 单个订阅函数生成的执行代码

对于sync,async,promise中的每一类的`单个订阅函数`生成执行代码的主体逻辑也是一致的,比如promise形式的订阅函数都需要接收订阅函数返回的promise,并在该promise上添加成功或者失败的回调。由于这样的性质,统一收敛在了`HookCodeFactory.callTap`方法上;

上面的`HookCodeFactory.create`方法提供了`onError`、`onResult`、`onDone`等默认值,本小结的两个案例中的SyncHook和AsyncSeriesHook默认继承这几个值,并且由于这两个属于`Basic`场景没有用到`onResult`,用的是`onDone`(二者是互斥的,只会用到一个,后面会分析到)。

```javascript

// HookCodeFactory.js

callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {

let code = "";

// ...

code = var _fn${tapIndex} = ${this.getTapFn(tapIndex)};n;

const tap = this.options.tapstapIndex;

switch (tap.type) {

代码语言:txt复制
  case "sync":
代码语言:txt复制
     //...
代码语言:txt复制
     break;
代码语言:txt复制
  case "async":
代码语言:txt复制
     //...
代码语言:txt复制
     break;
代码语言:txt复制
  case "promise":
代码语言:txt复制
      //...
代码语言:txt复制
     break;

}

return code;

}

代码语言:txt复制
针对这两个hook的`单个函数的执行模板`如下表:

​

![请在此添加图片描述](https://ask.qcloudimg.com/raw/yehe-b343db5317ff8/md86jauxl2.png?qc_blockWidth=793&qc_blockHeight=366)

​

onDone下的模板和onResult的模板略有差异,这里的两个Hook都是使用`onDone`下的模板。将onDone的默认值带入onDone下的模板中,就可以得到`单个订阅函数`的生成代码

# 2. Basic | Bail | Waterfall | Loop

-  `Xxx(Basic)Hook`: 类似EventEmitter这类发布订阅库实现的效果是执行每一个订阅函数,但不关心函数的执行结果;
-  `XxxBailHook`: 相较于BasicHook,会对每个订阅函数的执行结果进行判断,如果是undefined则进入下一个订阅函数的执行,否则直接结束后面流程将值返回给发布者;
-  `XxxWaterfallHook`: 相较于BasicHook,同样会对每个订阅函数的执行结果进行判断, 如果不是undefined,则将该结果作为下一个订阅函数的第一个入参传递
-  `XxxLoopHook`: 同样会对每个订阅函数的执行结果进行判断,如果当前订阅函数返回结果是undefined则继续执行下一个订阅函数,不为undefined则跳转至第一个订阅函数从头开始执行。这样一直循环直至所有的订阅函数的返回结果均为undefined。另外该特性依赖`Series`特性,因此有SyncLoopHook和AsyncSeriesLoopHook,没有AsyncParallelLoopHook。

根据上面的特性描述,看到tapable提供的hooks可以根据订阅函数的执行结果的不同来判断后面的流程,是终止还是接着走,又或者将上一个订阅函数的执行结果传作为下一个订阅函数的入参等。

以SyncXxxHook为例对比这四个特性的表现和实现。

```javascript

const synHook = new SyncHook('arg');

synHook.tap('test', (arg) => console.log('1', arg) )

synHook.tap('test', (arg) => console.log('2', arg) )

synHook.call('parameter');

const synBailHook = new SyncBailHook();

synBailHook.tap('test', ()=> console.log(1))

synBailHook.tap('test', ()=> console.log(2))

synBailHook.call();

const synWaterHook = new SyncWaterfallHook('arg1');

synWaterHook.tap('test', ()=> console.log(1))

synWaterHook.tap('test', ()=> console.log(2))

synWaterHook.call('parameter');

const synLoopHook = new SyncLoopHook();

synLoopHook.tap('test', ()=>console.log(1))

synLoopHook.tap('test', ()=>console.log(1))

synLoopHook.call()

代码语言:txt复制
下述表格,每行对应一个特性,通过生成的代码可以看到和逻辑图显示逻辑一致

​

![请在此添加图片描述](https://ask.qcloudimg.com/raw/yehe-b343db5317ff8/lu306gekkm.png?qc_blockWidth=793&qc_blockHeight=592)

​

![请在此添加图片描述](https://ask.qcloudimg.com/raw/yehe-b343db5317ff8/23rl90gmap.png?qc_blockWidth=793&qc_blockHeight=538)

​

![请在此添加图片描述](https://ask.qcloudimg.com/raw/yehe-b343db5317ff8/frjb9lpzp7.png?qc_blockWidth=793&qc_blockHeight=533)

​

![请在此添加图片描述](https://ask.qcloudimg.com/raw/yehe-b343db5317ff8/eu6nu23plf.png?qc_blockWidth=793&qc_blockHeight=442)

​

上一节关于Sync/Async特性时有说到`HookCodeFactory.create`提供了默认的`onResult`和`onDone`等值,Sync(Basic)Hook特性依然继承自默认值,在这里`SyncBailHook`、`SyncWaterfallHook`、`SyncLoopHook`等子类中根据该hook自身特性提供了自己的`onResult`值

HookCodeFactory.create对于sync类型的hook提供的onResult、onDone等默认值

```javascript

// ...

onResult: result => return ${result};n, // Bail会调用这个默认值返回结果

onDone: () => "", // <=> ()=> return ""

代码语言:txt复制
`HookCodeFactory.createTap`中提供的关于sync类型hook的模板

```javascript

// onResult情况下的模板

var _result${tapIndex} = _fn${tapIndex}(${this.args(/..参数./)});

${onResult(_result${tapIndex})}

// onDone情况下的模板

// Sync(Basic)Hook,没有提供onResult,会调用这个版本

`_fn${tapIndex}(${this.args(/..参数./)})

${onDone()}`

代码语言:txt复制
​

![请在此添加图片描述](https://ask.qcloudimg.com/raw/yehe-b343db5317ff8/8whvfytr1c.png?qc_blockWidth=793&qc_blockHeight=579)

​

![请在此添加图片描述](https://ask.qcloudimg.com/raw/yehe-b343db5317ff8/wnyqg1l4cm.png?qc_blockWidth=793&qc_blockHeight=276)

​

其他部分细节:

-  `Waterfall`和`Basic`的差别是,会将上一个订阅函数的返回值(不是undefined)当做下一个订阅函数的第一个入参。`Waterfall`的这种特性要求每个订阅函数有一个入参,因此在`XxxWaterfallHook`的构造函数中会要求传递至少一个参数;
-  `loop`特性在最外层添加了`do-while`代码片段,看起来是加在了`content`外层的部分则认为可以在`HookCodeFactory.create`里面添加外围部分就像`Sync|Async`中看到Promise外面也添加一部分外围代码。实际上是不行的,因为create只感知sync、async、promise不感知loop这类性质,只应该提供这三个特性相关的公共逻辑,因此加不了。所以单独引入了一个方法callTapsLooping,来添加这部分逻辑。但是单个订阅函数后面的逻辑依然是用到了callTapsLooping提供的onResult被callTap调用生成衔接代码;

## 小结

-  首先我们看到这四个Hook都调用了callTapsSeries方法,该方法是起到将多个订阅函数的执行代码串起来的作用,后面小结会具体分析该方法
-  另外我们看到`Bail`、`Waterfall`、`Loop`等特性都需要根据订阅函数的`执行结果`进行一些判断并提供了`onResult`用来接收`执行结果`生成相邻订阅函数的`衔接`逻辑。而`Basic`不要求获取执行结果只提供了`onDone`。可以明显感受到onResult的作用,以及和onDone的差异。

# 3. Parallel | Series

| 标题 | Parallel: 并行执行 | Series: 串行执行 |
|:----|:----|:----|
| 订阅函数执行逻辑 | ​ | ​ |


上面提到的两类特性Sync|Async, Basic|Bail|Waterfall|Loop都是和`单个订阅函数`的执行代码生成有关主要是`HookCodeFacory.callTap`方法中。

-  Sync|Async特性由callTap中的switch-case来定义sycn、async、promise的模板,该模板中会预留onResult和onDone插槽,具体的逻辑交给各具体的子类实现。
-  Basic、Bail、Waterfall、Loop特性的差别由子类中的`onResult`决定,然后`callTap`调用`onResult`生成差异代码。

而这里的Parallel和Series主要和`多个订阅函数`间执行关系有关:`并行` or `串行`。并且 。此外对于多个同步函数来说只能串行执行,所以这里的特性都是针对异步订阅函数的。

下面我们具体看下内部串行和并行是如何设计和实现的

## Series

这个特性实际上需要区分同步和异步,异步需要在回调里面去调用下一个订阅函数的执行,而同步则不需要,因为同步默认就是串行也只能是串行;同步的钩子名称省略了该关键词,实际是`Sync(SeriesXxx)Hook`

​

![请在此添加图片描述](https://ask.qcloudimg.com/raw/yehe-b343db5317ff8/ouxuv300w4.png?qc_blockWidth=793&qc_blockHeight=614)

​

![请在此添加图片描述](https://ask.qcloudimg.com/raw/yehe-b343db5317ff8/f09sem6ote.png?qc_blockWidth=793&qc_blockHeight=140)

​

看下具体的源码实现

​

![请在此添加图片描述](https://ask.qcloudimg.com/raw/yehe-b343db5317ff8/0hl7qkh7jo.png?qc_blockWidth=793&qc_blockHeight=486)

​

![请在此添加图片描述](https://ask.qcloudimg.com/raw/yehe-b343db5317ff8/t18xni5yk2.png?qc_blockWidth=793&qc_blockHeight=66)

​

实际上由于javascript提供[Function Hoisting](https://developer.mozilla.org/en-US/docs/Glossary/Hoisting),顺序遍历(比如这里让_next0在_fn0的后面)也是行得通的。不过得是函数声明的形式,如果是函数表达式则不行。

另外`callTapSeries`调用`callTap`的实参中看到,onResult和onDone只会有一个生效。在这里的两个`Basic`特性的hook中,子类都没有提供onResult,如下SyncHook。

```javascript

class SyncHookCodeFactory extends HookCodeFactory {

content({ onError, onDone, rethrowIfPossible }) {

代码语言:txt复制
  return this.callTapsSeries({ // 未提供onResult
代码语言:txt复制
     onError: (i, err) => onError(err),
代码语言:txt复制
     onDone,
代码语言:txt复制
     rethrowIfPossible
代码语言:txt复制
  });

}

}

代码语言:txt复制
这里其实看到`onResult`和`onDone`的区别和用途,首先二者都是传递给`callTap`的参数,当需要根据当前订阅函数的执行结果进行一些判断时(如XxxBailHook等等)就传递`onResult`,实际上`onResult是在onDone增强`即添加一些条件判断,在各子类Hook中如果提供了onResult,其内部一定会调用onDone(这也解释了为什么调用callTap时二者只需其一);这一点在`callTapsSeries`传入的onResult也能看出,传递done(实际是传递给callTap的onDone)给子类提供的onResult函数。

以SyncBailHook为例再验证下上面的关于onResult和onDone的说法,见下图:

![请在此添加图片描述](https://ask.qcloudimg.com/http-save/yehe-10164320/160daeaa6b8d72e78608f56a06ff4add.png?qc_blockWidth=793&qc_blockHeight=443)

## Parallel

当然异步才有资格谈并行,即同时执行多个异步订阅函数,并在回调中判断是否所有的订阅函数都执行完成。该特性相关的有AsyncParallelHook、AsyncParallelBailHook,这里以`AsyncParallel(Basic)Hook`为例介绍Parallel特性。

demo如下:

```javascript

const asyncParallelHookPromise = new AsyncParallelHook();

asyncParallelHookPromise.tapPromise('test', () => new Promise(resolve => setTimeout(() => { resolve(1) }, 1000)))

asyncParallelHookPromise.tapPromise('test', () => new Promise(resolve => setTimeout(() => { resolve(2) }, 1000)))

const p1 = asyncParallelHookPromise.promise();

代码语言:txt复制
生成的代码,简化后如下:

```javascript

(function anonymous() {

代码语言:txt复制
"use strict";
代码语言:txt复制
return new Promise((_resolve, _reject) => {
代码语言:txt复制
    // ...
代码语言:txt复制
    var _x = this._x;
代码语言:txt复制
    do {
代码语言:txt复制
        var _counter = 2; // 一共有两个异步函数
代码语言:txt复制
        // ...
代码语言:txt复制
        var _fn0 = _x[0];
代码语言:txt复制
        var _promise0 = _fn0();
代码语言:txt复制
        _promise0.then(_result0 => {
代码语言:txt复制
            // ...
代码语言:txt复制
            // if (--_counter === 0) _resolve(); 
代码语言:txt复制
        });
代码语言:txt复制
        var _fn1 = _x[1];
代码语言:txt复制
        var _promise1 = _fn1();
代码语言:txt复制
        _promise1.then(_result1 => {
代码语言:txt复制
            // ...
代码语言:txt复制
            // if (--_counter === 0) _resolve();
代码语言:txt复制
        });
代码语言:txt复制
    } while (false);
代码语言:txt复制
});

})

代码语言:txt复制

上面看到实现该能力的核心是,并发执行所有的异步函数,增加计数器,每个异步订阅函数执行完成以后计数器减一,减至为0时则完成整个执行过程。并行和串行的逻辑差异较大,串行需要考虑上下相邻的订阅函数的衔接,但串行不用,因此HookCodeFactory单独提供了生成并行逻辑的方法callTapsParallel

忽略这里do-while,这里没什么作用,猜测可能为了以后扩展loop特性预留的。过程如下:

  • 外围添加了计数器相关逻辑,当前是Basic特性,没有onResult,使用onDone,看到回调中将计数器减一然后判断是否为0.
  • 一个for循环,顺序生成每个订阅函数的执行代码
  • 同样是调用callTap传递onResult、onDone生成单个订阅函数的执行代码。

小结

  • 相同点:看到callTapsSeriescallTapsParallel的主要结构都是引入一个for循环遍历所有的订阅函数,并在for循环内部调用callTap为每一个订阅函数生成执行代码
  • 差异点:callTapsSeries生成的每个订阅函数有严格的执行顺序,上一个订阅函数执行完完成以后才会进入执行第二个订阅函数的执行逻辑中;而callTapsParallel生成的各订阅函数的执行逻辑中没有严格的执行顺序,这些订阅函数只有统一的终点就是当所有的订阅函数执行完成或有任何订阅函数返回非undefined的结果(前者是Baisc特性,后者是Bail特性)

总结

从上面给出的demo中首先能够看到提供的多种多样的hook具备了很多特性,可以满足很多复杂的场景。

  • HookCodeFactory.create & HookCodeFactory.callTap
代码语言:txt复制
- HookCodeFactory.create 根据sync、async、promise构造生成匿名函数的`整体结构`
- HookCodeFactory.callTap 根据sync、async、promise生成`单个订阅函数`相关代码片段Basic、Bail、Waterfall、loop:主要区别在于相邻订阅函数的衔接,由子类提供的
  • callTapsSeries、callTapsParallel:根据多个订阅函数的执行顺序来将callTap生成的代码连接起来
代码语言:txt复制
- callTapsSeries:严格串行执行
- callTapsParallel:并行

0 人点赞