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;

结语

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

相关推荐
那就可爱多一点点2 小时前
H5页面多个视频如何只同时播放一个?
前端·音视频
前端郭德纲4 小时前
浅谈React的虚拟DOM
前端·javascript·react.js
2401_879103685 小时前
24.11.10 css
前端·css
ComPDFKit6 小时前
使用 PDF API 合并 PDF 文件
前端·javascript·macos
yqcoder6 小时前
react 中 memo 模块作用
前端·javascript·react.js
优雅永不过时·7 小时前
Three.js 原生 实现 react-three-fiber drei 的 磨砂反射的效果
前端·javascript·react.js·webgl·threejs·three
神夜大侠9 小时前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱9 小时前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
柯南二号10 小时前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
wyy729310 小时前
v-html 富文本中图片使用element-ui image-viewer组件实现预览,并且阻止滚动条
前端·ui·html