Tapable解析

前言

作为webpack控制事件流的依赖库,tapable对于我们理解webpack、独立使用解决类似需求和方法设计参考大有裨益,本文中我将从最简单的实践开始拓展,由浅及深,由点及面,主打让大家无需下载即可看完源码。

目录

源码

选读

结语

源码解析

一个基本的SyncHook

该文基于的tapable版本为2.2.1,我们从一个最简单的例子开始理解tapable

引用自不必多谈,我们以SyncHook为例。先引用该库,接着创建一个实例,先不管里面的参数作用,我们只需要知道tap函数用来处理响应逻辑,call函数用来发送通知。

js 复制代码
const { SyncHook } = require("tapable")

// 声明一个同步hook的实例
const testHook = new SyncHook(["args1"])

// 注册响应事件
testHook.tap("tap1", res => console.log(res))

// 发送通知
setTimeout(() => testHook.call("chipi chipi chapa chapa"))

// 控制台打印出 chipi chipi chapa chapa

接下来我们抽丝剥茧,从最核心的逻辑看起。

classDiagram Hook <|-- SyncHook Hook : +Array _args Hook : +String name Hook : +Functions[] taps Hook : ... Hook : +compile() Function HookCodeFactory <|-- SyncHookCodeFactory HookCodeFactory : +setup() HookCodeFactory : +create() Function SyncHook <-- SyncHookCodeFactory class SyncHook class SyncHookCodeFactory { + content() String }

类图如上,SyncHook是一个函数,但是为了便于理解,我们可以将它视作一个类(毕竟JS的类其实就是一个构造函数),它是Hook的子类,同时关联一个工厂类的子类SyncHookCodeFactory

js 复制代码
function SyncHook(args = [], name = undefined) {
  const hook = new Hook(args, name);
  hook.compile = COMPILE;
  return hook;
}

SyncHook的创建函数如上,我们可以看出来SyncHook创建了一个Hook实例并返回,所以本质上得到的其实是一个Hook,这与我们的类图相符。按图索骥,我们来看看Hook是何方神圣,当我们new一个Hook并调用calltap时,它具体做了什么呢?我们挑核心部分来看一下。

先来看一下tap函数,本质就是对options做了处理,新增了我们传入的属性,并将这些信息赋值给taps数组。

js 复制代码
tap(options, fn) {
    if (typeof options === "string") {
       options = {
          name: options.trim()
       };
    }
    options = Object.assign({ type: "sync", fn }, options);
    // 将options里的函数赋值给taps,具体逻辑先不看,只需要知道函数的这个作用即可
    this._insert(options);
}

再来看一下callHook在初始化时把它赋值为CALL_DELEGATE,所以我们在调用call的时候实际调用的是CALL_DELEGATE,我整合了一下它的逻辑,可以看出call在被调用时会被重新赋值成compile的函数返回结果。

js 复制代码
const CALL_DELEGATE = function(...args) {
    this.call = this.compile({
       taps: this.taps,
       interceptors: this.interceptors,
       args: this._args,
       type: "sync"
    });
    return this.call(...args);
};

所以问题的关键就回到了compiler上,而SyncHook的函数代码告诉我们compiler在该函数被调用时会被赋值COMPILE,内容如下:

js 复制代码
const factory = new SyncHookCodeFactory();

const COMPILE = function(options) {
    factory.setup(this, options);
    return factory.create(options);
};

SyncHookCodeFactorysetupcreate方法继承自HookCodeFactory,看来答案在HookCodeFactory这两个方法中。

HookCodeFactory

顾名思义,这个类是一个关于代码的工厂类,事实确是如此,这个类的核心就是一个函数方法new Function,代码中充斥着很多拼接字符串的操作,因此要结合实际运行时的打点结果才好理解。

我们先来看上文中调用的setup,其逻辑相当简单,就是将options中的taps的函数取出赋值给_x属性,也就是我们上文中写的回调函数,我们只写了一个,所以taps里只有一个元素。

js 复制代码
setup(instance, options) {
    instance._x = options.taps.map(t => t.fn);
}

再来看create函数,它调用了contentWithInterceptors函数,该函数实际上调用了子类自己的content实现。

js 复制代码
create(options) {
    let fn;
    switch (this.options.type) {
       case "sync":
          fn = new Function(
             this.args(),
             '"use strict";\n' +
                this.header() +
                this.contentWithInterceptors({
                   onError: err => `throw ${err};\n`,
                   onResult: result => `return ${result};\n`,
                   resultReturns: true,
                   onDone: () => "",
                   rethrowIfPossible: true
                })
          );
          break;
    }
    return fn;
}

contentWithInterceptors(options) {
  return this.content(options);
}

SyncHookcontent实现如下,它调用了工厂类的callTapsSeries方法,该方法的工作就是根据类型输出对应的函数方法字符串。

js 复制代码
content({ onError, onDone, rethrowIfPossible }) {
    return this.callTapsSeries({
       onError: (i, err) => onError(err),
       onDone,
       rethrowIfPossible
    });
}

所以最终可以得到这样一个函数,也就是我们调用call时执行的函数,其中_fn0函数是我们通过tap方法注册的函数:

js 复制代码
function(args1) {
  "use strict";
  var _context;
  var _x = this._x;
  var _fn0 = _x[0];
  _fn0(args1);
}

至此,我们可以总结出整个tapable的基本运行流程:

graph LR A[声明对应Hook的代码处理子类] --> 定义代码生成逻辑 --> 创建该类的全局实例变量 B[声明具体的Hook实例] --> 初始化tap等成员变量 --> 赋值compiler C[调用tap] --> 将用户自定义的tap注入到Hook实例的taps成员变量中 D[调用call] --> 调用compiler方法获取返回的函数 --> 将taps信息注入到代码处理工厂类的_x成员变量中 --> 调用代码处理类的create方法创建函数 --> 调用contentWithInterceptors函数按照自定义的代码逻辑生成函数 --> 调用生成的函数

代入上述的例子,当我们执行第一句时

js 复制代码
const { SyncHook } = require("tapable")

// const factory = new SyncHookCodeFactory();

执行第二句时

js 复制代码
const testHook = new SyncHook(["hookName"])

// testHook._args = ["hookName"]
// testHook.compiler = (options) => {
//   factory.setup(this, options);
//   return factory.create(options);
// }
// testHook.tapAsync testHook.tapPromise被设置为无效的报错

执行第三句时

js 复制代码
testHook.tap("tap1", res => console.log(res))

// testHook.taps = [{
//   name: "tap1"
//   type: "sync"
//   fn: res => console.log(res)
// }]

执行第四句时

js 复制代码
testHook.call("chipi chipi chapa chapa")

// factory._x = [res => console.log(res)]
// testHook.call = function(args1) { 
//   "use strict"; 
//   var _context; 
//   var _x = [res => console.log(res)]; 
//   var _fn0 = res => console.log(res);
//   _fn0(args1); 
// }

可见tapable库对这部分功能做了有效的解耦:

  • HookCodeFactory类负责处理函数生成,该工厂类不直接定义不同hook的实现,只处理options和提供一些代码处理函数供调用;
  • Hook类负责处理calltap等函数逻辑,具体内容后文会涉及到;
  • 不同的Hook子类主要差异在于content函数的定义,通过影响contentWithInterceptors逻辑间接影响生成的代码内容。

Async

Sync AsyncPromise实际上是Hook类型外的另一种区分方式,决定我们tap出来什么类型的函数结构。所以你应该不难想到,声明不同类型的Hook,声明Sync/Async/Promise类型的tapcall函数,这三者相乘会有多少种情况...当然这些取决于实际运用时候的选择,本文会告诉你如何去进行选择。

我们先来看一下对Async类型的处理,异步又被分为多种类型,但有了上文的基础后下面的内容很容易理解。

AsyncParallelHook

我们以AsyncParallelHook为例,顾名思义,AsyncParallelHook为并行的异步方法,以它为例,我们来看一下它在实现上有哪些差异。

不考虑传参的不同,我们可以直接从content看起,可以看到这里直接调用了callTapsParallel方法。

js 复制代码
content({ onError, onDone }) {
    return this.callTapsParallel({
       onError: (i, err, done, doneBreak) => onError(err) + doneBreak(true),
       onDone
    });
}

我们将一开始的例子稍作改动:

js 复制代码
testHook.tapAsync("tap1", (res, errorHandler) => {
    console.log(res)
    errorHandler()
})

setTimeout(() => testHook.callAsync("地瓜地瓜", () => console.log("this is callback")), 3000)

// 输出结果为:
// 地瓜地瓜
// this is callback

当我们只有一个监听事件时,该方法直接调用了callTapsSeries,和SyncHook的逻辑类似,最终得到以下的callAsync函数:

js 复制代码
function (args1, _callback) {
  "use strict";
  var _context;
  var _x = this._x;
  var _fn0 = _x[0];
  _fn0(args1, (function(_err0) {
    if(err0) {
      _callback(err0);
    } else {
      _callback();
    }
  }))
}

当我们注册多个监听事件呢?比如我们添加一个监听事件,这时callTapsParallel函数调用逻辑会发生改变,其中的条件判断和字符串拼接不做赘述,可以得到如下的callAsync函数:

js 复制代码
function (args1, _callback) {
  "use strict";
  var _context;
  var _x = this._x;

  do {
    var _counter = 2;
    var _done = (function() {
      _callback();
    })
  
    if(_counter <= 0) break;
    var _fn0 = _x[0];
    _fn0(args1, (function(_err0) {
      if(err0) {
        if(_counter > 0) {
          _callback(_err0);
          _counter = 0;
        }
      } else {
        if(--_counter === 0) _done();
      }
    }))
  
    if(_counter <= 0) break;
    var _fn1 = _x[1];
    _fn1(args1, (function(_err1) {
      if(err1) {
        if(_counter > 0) {
          _callback(err1);
          _counter = 0;
        }
      } else {
        if(--_counter === 0) _done();
      }
    }))
  } while(false);
}

Promise

除了上文的同步异步外,tapable还支持Promise,我们着重来看一下从create开始具体做了哪些差异化处理。

在揭晓具体实现之前,我们可以思考一下如果是自己来设计的话会如何处理。首先,和之前的格式一样,生成的依然是一个函数,函数的返回值应该是一个Promise,所以假设testHook.xcall函数,那么testHook.x => new Promise

那么tap如何设计呢?这里我们只考虑tapPromise的设计,参考async,由于Promise的逻辑在声明时就会执行,所以我们只能设计成testHook.tapPromise("listener1", () => new Promise)

再考虑回调,asynccallback被作为参数传入,因此这部分的实现大概率雷同,resolve就是最好的载体。tapPromise可以通过resolve控制自己的then执行,还可以通过调用callPromiseresolve来控制回调,最简单的处理就是把回调逻辑放到callPromisethen中,这样不resolve就不会执行then,符合我们的要求。据此,我们可以大致推断出tapPromisecallPromise的写法如下:

js 复制代码
testHook.tapPromise("listener1", () => new Promise((resolve, reject) => {
  ...
  resolve()
}))

testHook.callPromise().then(() => console.log("this is a call promise"))

接下来,我们尝试去反写出具体实现,先仿照上文搭个架子。

js 复制代码
function (args1) {
  "use strict";
  var _context;
  var _x = this._x;
  return new Promise((function(_resolve, _reject) {
    do {
      var _counter = 2;
      var _done = (function() {
        _resolve();
      }) 
      
      if(_counter <= 0) break;
      var _fn0 = _x[0];
      var _promise0 = _fn0()
      _promise0.then(() => {
        if (-- _counter <= 0) _done()
      })
      
      if(_counter <= 0) break;
      var _fn1 = _x[1];
      var _promise1 = _fn1()
      _promise1.then(() => {
        if (-- _counter <= 0) _done()
      })
      
    } while(false)
  })
}

对比一下实际生成的代码,我们发现多了类型校验和error的处理等等,思路大致相同。

js 复制代码
function (args1) {
  "use strict";
  var _context;
  var _x = this._x;
  return new Promise((function(_resolve, _reject) {
    var _sync = true;
    function _error(_err) {
      if(_sync)
        _resolve(Promise.resolve().then((function() { throw _err; })));
      else
      _reject(_err);
    };
    do {
      var _counter = 2;
      var _done = (function() {
        _resolve();
      })
    
      if(_counter <= 0) break;
      var _fn0 = _x[0];
      var _hasResult0 = false;
      var _promise0 = _fn0(test)
      if (!_promise0 || !_promise0.then)
        throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise0 + ')')
      _promise0.then((function(_result0) {
        _hasResult0 = true;
        if(--_counter === 0) _done();
      }), function(_err0) {
        if (_hasResult0) throw _err0;
        if(_counter > 0) {
          _error(_err0);
          _counter = 0;
        }
      })
  
      if(_counter <= 0) break;
      var _fn1 = _x[1];
      var _hasResult1 = false;
      var _promise1 = _fn1()
      if (!_promise1 || !_promise1.then)
        throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise1 + ')')
      _promise1.then((function(_result1) {
        _hasResult1 = true;
        if(--_counter === 0) _done();
      }), function(_err1) {
        if (_hasResult1) throw _err1;
        if(_counter > 0) {
          _error(_err1);
          _counter = 0;
        }
      })
    } while(false);
    _sync = false;
  }));
}

拓展

我们接下来再简单介绍其他几种类型Hook的实现。

  • AsyncParallelBailHook

对用例稍加改动。

js 复制代码
testHook.tapAsync("listener1", (res1, errorHandler) => {
    console.log(res1)
    errorHandler(null, "1")
})

上文我们谈到AsyncParallelHook是并行的异步hook,它的并行性表现在注册的tap函数执行是异步的,但是callback在所有tap函数执行完成后才会回调;而bail的含义为保险丝,这种BailHook有点类似于Promise.race,只要有一个回调函数的返回值不为null就会执行callback逻辑,并且其他的tap逻辑不再执行,这是两者最大的区别。具体实现如下。

js 复制代码
function (args1, _callback) {
  "use strict";
  var _context;
  var _x = this._x;

  var _results = new Array(2);
  var _checkDone = function() {
    for(var i = 0; i < _results.length; i++) {
      var item = _results[i];
      if(item === undefined) return false;
      if(item.result !== undefined) {
        _callback(null, item.result);
        return true;
      }
      if (item.error) {
        _callback(item.error);
        return true;
      }
    }
    return false;
  }
  
  do {
    var _counter = 2;
    var _done = (function() {
      _callback();
    })
  
    if(_counter <= 0) break;
    var _fn0 = _x[0];
    _fn0(args1, (function(_err0, _result0) {
      if(err0) {
        if (_counter > 0) {
          if(0 < _results.length && ((_results.length = 1), (_results[0] = { error: _err0 }), _checkDone())) {
            _counter = 0;
          } else {
            if(--_counter === 0) _done();
          }
        }
      } else {
        if (_counter > 0) {
          if(0 < _results.length && (_result0 !== undefined && (_results.length = 1), (_results[0] = { result: _result0 }), _checkDone()) {
            _counter = 0;
          } else {
            if(--_counter === 0) _done();
          }
        }
        if(--_counter === 0) _done();
      }
    }))
  
    if(_counter <= 0) break;
    if (1 >= _results.length) {
      if(--_counter === 0) _done();
    } else {
      var _fn1 = _x[1];
      _fn1(args1, (function(_err1, _result1) {
        if(err1) {
          if (_counter > 0) {
            if(1 < _results.length && ((_results.length = 1), (_results[1] = { error: _err1 }), _checkDone())) {
              _counter = 0;
            } else {
              if(--_counter === 0) _done();
            }
          }
        } else {
          if (_counter > 0) {
            if(1 < _results.length && (_result1 !== undefined && (_results.length = 1), (_results[1] = { result: _result1 }), _checkDone()) {
              _counter = 0;
            } else {
              if(--_counter === 0) _done();
            }
          }
          if(--_counter === 0) _done();
        }
      }))
    }
  } while(false);
}

这里的_results接收的是用户在tap中自定义的结果,在第二个函数参数里传入,所有的tap函数执行是串行的,一旦某个tap中回传的结果是有效的就会立马停止执行其他函数。

  • AsyncSeriesHook

顾名思义,这是一个串行执行的异步hook,我们将用例代码修改成如下:

js 复制代码
testHook.tapAsync("listener1", (test, cb) => {
    console.log("test1", test);
    cb();
})

这个hook相对简单,保证tap函数依次执行即可,callAsync函数源码如下:

js 复制代码
function(args1, _callback) {
  "use strict";
  var _context;
  var _x = this._x;
  
  function _next0() {
    var _fn1 = _x[1];
    _fn1(args1, (function(_err1) {
      if(err1) {
        _callback(err1);
      } else {
        _callback();
      }
    }))
  }
  
  var _fn0 = _x[0];
  _fn0(args1, (function(_err0) {
    if(err0) {
      _callback(err0);
    } else {
      _next0();
    }
  }))
}
  • AsyncSeriesBailHook

根据上文我们不难理解,这个hook就是串行+block,一旦有一个tap函数正常返回,则不再执行其他tap函数,其实现机制和上文中的BailHook相同。

callAsync函数源码如下:

js 复制代码
function(args1, _callback) {
  "use strict";
  var _context;
  var _x = this._x;
  
  function _next0() {
    var _fn1 = _x[1];
    _fn1(args1, (function(_err1, _result1) {
      if(_err1) {
        _callback(_err1);
      } else {
        if(_result1 !== undefined) {
          _callback(null, _result1);
        } else {
          _callback()
        }
      }
    }
  }
  
  var _fn0 = _x[0];
  _fn0(args1, (function(_err0, _result1) {
    if(_err0) {
      _callback(_err0);
    } else {
      if(_result0 !== undefined) {
        _callback(null, _result1);
      } else {
        _next0()
      }
    }
  }
}
  • AsyncSeriesLoopHook

看到loop,顾名思义,这个hook和循环相关,具体的使用场景确实也比较特别,简单说就是允许自定义逻辑里面通过控制返回值来重复执行,可以通过下面的例子来理解。

js 复制代码
let count = 1;

testHook.tapAsync("listener1", (res1, errorHandler) => {
  if (count < 5) {
    errorHandler(null, ++ count)
  } else {
    errorHandler()
  }
})
testHook.tapAsync("listener2", (res2, errorHandler) => {
    console.log(res2)
    errorHandler()
})

testHook.callAsync("chipi chipi chapa chapa", () => console.log("this is a callback"))

上述的例子中,先循环执行四次第一个tap,在最后一个执行完成后执行第二个tap。看下面代码也很容易理解具体实现,传入的result不为undefined时会循环执行所有tap,可以运用在有重试机制的场景里。

js 复制代码
function(args1, _callback) {
  "use strict";
  var _context;
  var _x = this._x;
  
  var _looper = (function() {
    var _loopAsync = false;
    var _loop;
    do {
      _loop = false;
      function _next0() {
        var _fn1 = _x[1];
        _fn1(args1, (function(_err1, _result1) {
          if(_err1) {
            _callback(_err1);
          } else {
            if(_result1 !== undefined) {
              _loop = true;
              if (_loopAsync) _looper();
            } else {
              if (!_loop) {
                _callback();
              }
            }
          }
        })
      }
      
      var _fn0 = _x[0];
      _fn0(args1, (function(_err0, _result0) {
        if(_err0) {
          _callback(_err0);
        } else {
          if(_result0 !== undefined) {
            _loop = true;
            if (_loopAsync) _looper();
          } else {
            if (!_loop) {
              _next0();
            }
          }
        }
      })
    } while(_loop);
    _loopAsync = true;
  })
  _looper();
}
  • AsyncSeriesWaterfallHook

看到waterfall应该很好联想该hook的作用,它会将上一个tap的结果作为参数传入下一个tap,代码也比较简单,直接通过修改this._args来实现传递结果。

js 复制代码
function(args1, _callback) {
  "use strict";
  var _context;
  var _x = this._x;
  
  function _next0() {
    var _fn1 = _x[1];
    _fn1(_result0, (function(_err1, _result1) {
      if(_err1) {
        _callback(_err1);
      } else {
        if(_result1 !== undefined) {
          this._args[0] = _result1;
          _callback();
        }
      }
    })
  }
      
  var _fn0 = _x[0];
  _fn0(args1, (function(_err0, _result0) {
    if(_err0) {
      _callback(_err0);
    } else {
      if(_result0 !== undefined) {
        this._args[0] = _result0;
        _next0();
      }
    }
  })
  • SyncBailHook

以下的几个同步hook和异步hook的作用相同,可以直接看具体的代码。

js 复制代码
function(args1) {
  "use strict";
  var _context;
  var _x = this._x;
  
  var _fn0 = _x[0];
  var _result0 = _fn0(args1);
  if(_result0 !== undefined) {
    return _result0;
  } else {
    var _fn1 = _x[1];
    var _result1 = _fn1(args1);
    if(_result1 !== undefined) {
      return _result1;
    } else {}
  }
}
  • SyncLoopHook

这里需要注意当tap没有返回值时才会循环,与异步的逻辑相反。

js 复制代码
function(args1) {
  "use strict";
  var _context;
  var _x = this._x;
  
  var _loop;
  do {
    _loop = false;
    
    var _fn0 = _x[0];
    var _result0 = _fn0(args1);
    if(_result0 !== undefined) {
      _loop = true;
    } else {
      if (!_loop) {
        var _fn1 = _x[1];
        var _result1 = _fn1(args1);
        if(_result1 !== undefined) {
          _loop = true;
        } else {
          if (!_loop) {}
        }
      }
    }
  } while(_loop);
}
  • SyncWaterfallHook
js 复制代码
function(args1) {
  "use strict";
  var _context;
  var _x = this._x;
  
  var _fn0 = _x[0];
  var _result0 = _fn0(args1);
  if(_result0 !== undefined) {
    this._args[0] = _result0;
    var _fn1 = _x[1];
    var _result1 = _fn1(_result0);
    if(_result1 !== undefined) {
      return _result1;
    }
  }
}

通过和上一部分sync的比较,我们可以发现call的类型影响的是create逻辑,看代码其实就是影响回调的方式,同步call无回调,异步call有回调。

js 复制代码
create(options) {
    ...
    case "sync":
      fn = new Function(
        this.args(),
        '"use strict";\n' +
        this.header() +
        this.contentWithInterceptors({
          onError: err => `throw ${err};\n`,
          onResult: result => `return ${result};\n`,
          resultReturns: true,
          onDone: () => "",
          rethrowIfPossible: true
        })
      );
      break;
      case "async":
        fn = new Function(
          this.args({
            after: "_callback"
          }),
          '"use strict";\n' +
          this.header() +
          this.contentWithInterceptors({
            onError: err => `_callback(${err});\n`,
            onResult: result => `_callback(null, ${result});\n`,
            onDone: () => "_callback();\n"
          })
        );
        break;
    ...
}
  • interceptors

顾名思义,拦截器,允许我们在指定的时机插入一些操作,它通过intercept方法注入触发,从代码中我们可以看到有以下几个属性。

js 复制代码
testHook.intercept({
  // 修改输出的是注册的tap函数设置
  register: opt => return opt;
  
  // tap call和loop使用context与否的开关
  context: false,
  
  call: args => console.log(args);
  
  // args也是注册的tap函数设置 并且允许使用context
  tap: args => console.log(args);
  
  // 剩余处理逻辑的拦截器
  loop error result done
})

register实际可能执行的时机有两处,但是在intercept逻辑中会去判断taps长度,在_runRegisterInterceptors中会去判断interceptor的长度,所以无论intercepttap的顺序如何,实际只有一处会被执行。

剩余的call等拦截器都会被写入到生成的代码中执行。

  • 优先级

tapable还额外提供了stagebefore字段来允许用户自定义优先级,简单易懂,但是从代码来看两者无法结合使用,我们先看一个和before相关的例子。

js 复制代码
testHook.tap("listener2", (res1) => console.log("this is 2"))

testHook.tap({
  name: "listener1",
  before: ["listener2"]
}, () => console.log("this is 1"))

// 输出结果
// this is 1
// this is 2

再看一个stage相关的例子。

js 复制代码
testHook.tap({
  name: "listener2",
  stage: 4
}, () => console.log("this is 2"))

testHook.tap({
  name: "listener1",
  stage: 3
}, () => console.log("this is 1"))

// 输出结果
// this is 1
// this is 2

如果我们同时使用呢,这时我们需要看一下具体代码。

js 复制代码
let i = this.taps.length;
while (i > 0) {
  i--;
  const x = this.taps[i];
  this.taps[i + 1] = x;
  const xStage = x.stage || 0;
  if (before) {
    if (before.has(x.name)) {
      before.delete(x.name);
      continue;
    }
    if (before.size > 0) {
      continue;
    }
  }
  if (xStage > stage) {
    continue;
  }
  i++;
  break;
}
this.taps[i] = item;

假设tap1的stage为3,tap2的stage为4,按照stage,tap1先于tap2执行,这时我们假设tap2的before为包含tap1,我们演示一下上述变化。

  1. 直接添加tap1,taps为[tap1];
  2. 进入循环后i变成0,x为tap1,taps为[tap1, tap1],此时由于before.has(tap1),所以直接跳出循环,taps为[tap2, tap1]。

但是实际运用的时候伴随场景复杂很难保证不起冲突,stage的场景其实涵盖了before,因此无需同时使用。

整体概览

通过上面的源码分析,我们从功能角度可以梳理出以下结论:

  • ParalllelHook:主要用于异步场景,允许异步hook并行,实际上是对同步hook场景下允许使用callback的补充;
  • SeriesHook:与上述相对的串行场景,这两者对于同步hook都是不存在的;
  • BailHook:提供了熔断机制,类似于race;
  • LoopHook:提供了循环机制,同/异步hook在细节上有所不同;
  • WaterfallHook:提供了瀑布流机制,上一个执行结果可以作为下一个输入参数。

更重要的是关于Sync AsyncPromise的使用,我们将详细谈谈这一部分在使用时如何选择。

  • call的类型选择直接决定了实际生成的call函数框架,我们可以看以下实际使用到calltype的逻辑部分。
js 复制代码
switch (this.options.type) {
    case "sync":
       fn = new Function(
          this.args(),
          '"use strict";\n' +
             this.header() +
             this.contentWithInterceptors({
                onError: err => `throw ${err};\n`,
                onResult: result => `return ${result};\n`,
                resultReturns: true,
                onDone: () => "",
                rethrowIfPossible: true
             })
       );
       break;
    case "async":
       fn = new Function(
          this.args({
             after: "_callback"
          }),
          '"use strict";\n' +
             this.header() +
             this.contentWithInterceptors({
                onError: err => `_callback(${err});\n`,
                onResult: result => `_callback(null, ${result});\n`,
                onDone: () => "_callback();\n"
             })
       );
       break;
    case "promise":
       let errorHelperUsed = false;
       const content = this.contentWithInterceptors({
          onError: err => {
             errorHelperUsed = true;
             return `_error(${err});\n`;
          },
          onResult: result => `_resolve(${result});\n`,
          onDone: () => "_resolve();\n"
       });
       let code = "";
       code += '"use strict";\n';
       code += this.header();
       code += "return new Promise((function(_resolve, _reject) {\n";
       if (errorHelperUsed) {
          code += "var _sync = true;\n";
          code += "function _error(_err) {\n";
          code += "if(!_sync)\n";
          code +=
             "_resolve(Promise.resolve().then((function() { throw _err; })));\n";
          code += "else\n";
          code += "_reject(_err);\n";
          code += "};\n";
       }
       code += content;
       if (errorHelperUsed) {
          code += "_sync = false;\n";
       }
       code += "}));\n";
       fn = new Function(this.args(), code);
       break;
}

看到这里应该很自然地能够想到我们之前提到的生成的代码中,最外层的函数内容实际上就是由这部分负责生成的,主要是入参和接受函数结果的方式,这部分不受我们选择的具体hook类型影响,所以即使是同步hook也可以选择callAsync

  • 具体的内容逻辑是由tap类型决定的,为了避免交叉使用的情况,同步hook设置使用tapAsynctapPromise无效,而异步hook不支持同步调用,这是合理的,我们以SyncHook为例,假如我们允许使用tapAsync,那么生成的代码会变成如下。
js 复制代码
function(args1) { 
  "use strict"; 
  var _context; 
  var _x = this._x;
  
  var _fn0 = _x[0]
  function _next0() {
    var _fn1 = _x[1]; 
    _fn1(args1, (function(_err1) {
      if(_err1) {
        throw _err1;
      } else {}
    }));
  }
  
  var _fn0 = _x[0]; 
  _fn0(args1, (function(_err0) {
    if(_err0) {
      throw _err0;
    } else {
      _next0();
    }
  }));
 
}

乍一看这么做也没有问题,但我们和AsyncSeriesHook比较会发现这种写法和直接用AsyncSeriesHook没有区别,且后者还支持callback,所以对不同hook限制用法主要便于用户理解和后续迭代管理。

细节选读

  • 仓库代码中存在很多子类,按照一般写法,我们会写成:
js 复制代码
class A extends B {}

但是实际上,extends是ES6支持的特性,本质上还是对构造函数进行操作,库中用了更简单的写法,直接赋值constructorconstructor本身返回一个实例,所以用一个返回指定实例的函数重写constructor即可。

js 复制代码
function A() {
  const b = new B();
  b.constructor = A;
  ...
  return b;
}
  • 当连续的同步函数数量超过22时,从后往前每22个同步函数会被编组为一个_next函数,mark一下,目前还不了解用意。
js 复制代码
let unrollCounter = 0;
for (let j = this.options.taps.length - 1; j >= 0; j--) {
    const i = j;
    const unroll = current !== onDone && unrollCounter++ > 20;
    if (unroll) {
       unrollCounter = 0;
       code += `function _next${i}() {\n`;
       code += current();
       code += `}\n`;
       current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`;
    }
    const done = current;
    const doneBreak = skipDone => {
       if (skipDone) return "";
       return onDone();
    };
    const content = this.callTap(i, {
       onError: error => onError(i, error, done, doneBreak),
       onResult:
          onResult &&
          (result => {
             return onResult(i, result, done, doneBreak);
          }),
       onDone: !onResult && done,
       rethrowIfPossible:
          rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
    });
    current = () => content;
}
  • 防止原型污染

这是我们自己封装package时很容易遇到的问题,特别是merge等操作很容易被原型污染攻击,需要我们对原型链进行特别处理。

js 复制代码
SyncHook.prototype = null;

结语

作为一个严谨的三方库,还有很多疑问笔者自己尚未思考清楚,有很多代码细节没有一一列举,后续会持续补充。

相关推荐
百万蹄蹄向前冲1 分钟前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳58139 分钟前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路1 小时前
GeoTools 读取影像元数据
前端
ssshooter1 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry2 小时前
Jetpack Compose 中的状态
前端
dae bal3 小时前
关于RSA和AES加密
前端·vue.js
柳杉3 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog3 小时前
低端设备加载webp ANR
前端·算法
LKAI.3 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi
刺客-Andy4 小时前
React 第七十节 Router中matchRoutes的使用详解及注意事项
前端·javascript·react.js