0.1.0 • Published 3 years ago

@copyist/tapable v0.1.0

Weekly downloads
4
License
ISC
Repository
github
Last release
3 years ago

tapable

类似EventListener,用于发布订阅事件,模仿tapable(v2.2.0) 用于webpack实现plugin体系

方法执行方式备注
SyncHook同步串行-
SyncBailHook同步串行返回值不是undefined,跳过后续tap执行并返回
SyncWaterfallHook同步串行上一个tap返回值传给下一个tap
SyncLoopHook同步循环返回值不是undefined,循环。多个taps时返回不是undefined,从第一个tap重新循环
AsyncParallelHook异步并发-
AsyncParallelBailHook异步并发返回值不是undefined,跳过后续tap执行并返回
AsyncSeriesHook异步串行-
AsyncSeriesLoopHook异步循环返回值不是undefined,循环。多个taps时返回不是undefined,从第一个tap重新循环
AsyncSeriesBailHook异步串行返回值不是undefined,跳过后续tap执行并返回
AsyncSeriesWaterfallHook异步串行上一个tap返回值传给下一个tap

方法

如上表格,tapable支持若干方法,主要区别为:

  • 同步(Sync) / 异步(Async)
  • 串行(Series) / 并发(Parallel)
  • 上一个的输出做下一个的输入(Waterfall) / tap返回值异常停止后续执行(Bail) / 不关心返回值

拦截器:包含四个方法

  • register:注册时触发,可以修改tap函数
  • call:执行call方法时触发的钩子
  • tap:执行每个tap时触发的钩子
  • loop:执行循环时触发的钩子

安装

yarn add @copyist/tapable

使用

  • 同步执行
const hook = new SyncHook(['arg1']);
hook.tap('tap1', (arg1) => {
  console.log('tap1执行:', arg1);
})
hook.tap('tap2', (arg1) => {
  console.log('tap2执行:', arg1);
})
hook.call('1');
// 输出:
// tap1执行: 1
// tap2执行: 1
  • 异步执行
const hook = new AsyncSeriesHook(['arg1']);
hook.tapAsync('tap1', (arg1, cb) => {
  console.log('tap执行:', arg1);
  cb();
})
hook.callAsync('1', () => {
  console.log('done')
});
hook.promise('2').then(() => {
  console.log('promise done')
})
// 输出:
// tap执行: 1
// done
// tap执行: 2
// promise done

解析:实现两个基类Hook、HookCodeFactory

Hook:注册tap事件、intercept拦截器

1、实现tap注册,tap队列有优先级,可以插队

  • 用stage表示优先级,默认为0,越小越优先
  • 用before表示插入到哪个前面

因为插入是按序的,所以调整顺序时使用插入排序

// 实现tap方法,并用stage表示优先级
class Hook {
  constructor() {
    this.taps = [];
  }

  /** 处理参数*/
  _dealOption(option){
    if(typeof option === 'string') {
      option = {
        name: option,
      }
    }
    return option;
  }
  /* 插入tap*/
  tap(option, fn){
    option = this._dealOption(option);
    option.fn = fn;
    const stage = option.stage || 0;
   
    let i = this.taps.length;
    /** 插入排序,stage字段判断是否向前 */
    while (i > 0) {
      const tap = this.taps[i - 1];
      this.taps[i] = tap;
      const tState = tap.stage || 0;
      /** stage小就往前排 */
      if (stage < tState) {
        i--;
        continue;
      }
      break;
    }
    this.taps[i] = option;
  }
}

测试

const hook = new Hook();
hook.tap('a',()=>{});
hook.tap({name:'b',stage: -1},()=>{})
console.log(hook.taps)
// 输出:
// {name: 'b', stage: -1, fn: [Function]}
// {name: 'a', fn: [Function]}
// 修改tap方法,添加before插队
tap(option, fn){
  option = this._dealOption(option);
  option.fn = fn;
  const stage = option.stage || 0;
  let before = new Set();
  if (typeof option.before === 'string') {
    before = new Set([option.before]);
  } else if (Array.isArray(option.before)) {
    before = new Set(option.before);
  }
  let i = this.taps.length;
  /** 插入排序,stage和before字段判断是否向前 */
  while (i > 0) {
    const tap = this.taps[i - 1];
    this.taps[i] = tap;
    const tState = tap.stage || 0;
    if (stage < tState) {
      i--;
      continue;
    }
    if (before.size) {
      before.delete(tap.name);
      i--;
      continue;
    }
    break;
  }
  this.taps[i] = option;
}

测试

const hook = new Hook();
hook.tap('a',()=>{});
hook.tap({name:'b',before: ['a']},()=>{})
console.log(hook.taps)
// 输出:
// {name: 'b', before: ['a'], fn: [Function]}
// {name: 'a', fn: [Function]}

2、实现interceptor注册 register:注册时触发,可以修改tap函数

// 添加intercept方法
intercept(option){
  // 构造函数需添加 this.interceptors = [];
  this.interceptors.push(option);
  if (option.register) {
    for (let i = 0; i < this.taps.length; i++) {
      // 修改tap
      this.taps[i] = this.interceptors[i].register(this.taps[i]);
    }
  }
}

测试

const hook = new Hook();
hook.tap('a',()=>{});
hook.intercept({
  register: tap => {
    tap.fn = () => {
      console.log('修改过的tap..')
    }
    return tap;
  }
})
console.log(hook.taps[0].fn());
// 输出:修改过的tap..

interceptor中的其他三个方法:call、tap和loop是执行时的钩子,是HookCodeFactory中的一部分

HookCodeFactory:构造可执行的代码

实现call、callAsync和promise等方法,生成一个Function再执行

看Hook类里面的代码

class Hook{
  /** _createCall得到一个Function */
  call = () => {
    this.call = hookCodeFactory._createCall("sync");
    return this.call(...args);
  };
}

1、第一次调用call方法时,会根据参数生成一个new Function 2、再次调用call方法,不用再次生成而是直接拿上次的Function执行。 3、为什么这样可以翻到下面的tapable相关问题

_createCall中包含tap函数和拦截器函数的执行,思路如下: 1、定义好各个变量 2、执行call的钩子函数 3、分别执行taps,先执行tap钩子函数,再执行tap

class HookCodeFactory {
  _createCall(type = 'sync'){
    // 1、定义变量
    let code = `
      var _taps = this.taps;
      var _x = _taps.map(tap => tap.fn);
      var _interceptors = this.interceptors;
    `
    // 2、执行拦截器call钩子函数
    // code += interceptorsCall();
  
    const _interceptorsTaps = this.interceptors.map(_ => _.tap).filter(Boolean);
  
    // 3、依次执行taps,先执行tap钩子再执行tap
    code += `
      const _interceptorsTaps = _interceptors.map(_ => _.tap).filter(Boolean);
      _taps.forEach((tap, tIdx) => {
        ${_interceptorsTaps.map((_tap, idx) => `_interceptorsTaps[${idx}](tap)`).join(';')}
        _x[tIdx]();
      })
    `;
    return new Function('', code);
  }
}

测试

const hook = new Hook();
hook.tap('a', () => { console.log('执行...') });
hook.call()
// 输出:执行...

可以通过下面方法看到生成的Function代码

const hook = new Hook();
hook.tap('a', () => {});
hook.call();
console.log(hook.call.toString());  // 输出生成的call函数

实现各种Hook

上面的例子,实现了一个简单的同步Hook,但要实现不同的Hook,需要改造,如图 npm.io

  • 将公共方法放在基类,实现类的不同方法在继承时重写

注册事件的tap、tapAsync和tapPromise方法

tap是一个事件,自然就有同步和异步之分,所以有这三个注册方法 不是所有Hook都有这三种注册方式,同步的Hook不能注册异步tap,因为同步Hook需要同步执行完事件

// 同步事件
hook.tap('sync', () => {
  console.log('注册sync tap')
});
// async事件
hook.tapAsync('async', (cb) => {
  console.log('注册async tap');
  cb();
});
// tapPromise事件
hook.tapPromise('tapPromise', () => {
  return new Promise((resolve) => {
    console.log('注册promise tap');
    resolve();
  })
});

执行事件的call、callAsync、promise

执行事件也同步和异步,所以也有三种 不是所有Hook都有这三种执行方式,异步Hook不能调用call,因为注册了异步的事件,执行也肯定需要异步完成。

// 同步执行
hook.call();
// async执行
hook.callAsync('async', () => {
  console.log('async执行完的回调');
});
// promise执行
hook.promise('promise').then(() => {
  console.log('promise执行完事件')
});

举例1、SyncBailHook

方法特性:同步串行,tap返回值不是undefined则跳过后续tap执行并返回 注册用的是tap,不能用tapAsync或tapPromise 执行事件可以是call、callAsync或promise 要实现的功能:

hook.tap('tap1', () => {
  console.log('tap1...');
});
hook.intercept({
  tap: () => {},
  call: () => {},
});
hook.call();

需要执行的代码:

  • 调用拦截器的call方法
  • 调用拦截器的tap方法
  • 调用同步的tap方法

构造的函数大致如下:

function anonymous(){
  // 1、定义变量
  var _taps = this.taps;
  var _x = _taps.map(tap => tap.fn);
  var _interceptors = this.interceptors;
  // 2、执行call钩子函数
  _interceptors.forEach(interceptor => {
    if (interceptor.call) {
      interceptor.call();
    }
  })
  // 3、依次执行taps
  const _interceptorsTaps = this.interceptors.map(_ => _.tap).filter(Boolean);
  let result;
  for(let i=0;i<_taps.length;i++){
    _interceptorsTaps.map((_interceptorsTap, idx) => {
      // 3.1 先执行tap拦截器函数
        _interceptorsTaps[idx](tap);
      }
      // 3.2 再执行tap函数
      result = _x[tIdx]();
      if (result !== undefined) {
        // 4、tap返回值不是undefined,不再往下执行
        return;
      }
  }
}

1、如果将hook.call()换成hook.callAsync(()=>{})

  • anonymous函数添加一个callback参数,执行时传入
callAsync(...args){
  this.callAsync = hookCodeFactory._createCall();
  this.callAsync(...args); // 将参数传入
}
  • 上面的第4点return改为
if (result !== undefined) {
  callback();
}

2、如果将hook.call()换成hook.promise().then(...)

  • 执行taps的部分要变成return new Promise()
  • 上面的第4点return改为
if (result !== undefined) {
  resolve();
}

举例2、AsyncSeriesHook

异步串行 注册用的是tap、tapAsync或tapPromise 执行事件可以是callAsync或promise,不能用call 要实现的功能:

hook.tapAsync('tap1', (cb) => {
  console.log('tap1...');
  cb();
});
hook.tapAsync('tap2', (cb) => {
  console.log('tap2...');
  cb();
});
hook.intercept({
  tap: () => {},
  call: () => {},
})
hook.callAsync();

需要执行的代码:

  • 调用拦截器的call方法
  • 调用拦截器的tap方法
  • 调用异步的tap方法

需要依次执行每个异步方法,所以将每个tap用函数包裹,一个执行完再执行下一个 有点类似于中间件 构造的函数大致如下:

function anonymous(callback){
  // 1、定义变量
  var _taps = this.taps;
  var _x = _taps.map(tap => tap.fn);
  var _interceptors = this.interceptors;
  // 2、执行call钩子函数
  _interceptors.forEach(interceptor => {
    if (interceptor.call) {
      interceptor.call();
    }
  })
  // 3、依次执行taps
  const _interceptorsTaps = this.interceptors.map(_ => _.tap).filter(Boolean);
  function next0(){
    function done(){
      typeof next1 !== 'undefined' ? next1() : callback();
    }
    // 3.1 先执行tap拦截器函数
      _interceptorsTaps[0](tap);
    // 3.2 再执行tap函数
    _x[0](done);
  };
  function next1(){
    function done(){
      typeof next2 !== 'undefined' ? next2() : callback();
    }
    // 3.1 先执行tap拦截器函数
      _interceptorsTaps[1](tap);
    // 3.2 再执行tap函数
    _x[1](done);
  }
  next0();
}

1、如果将tapAsync改为tapPromise,则

  • 3.2执行tap函数改为_x[0]().then(() => done());
  • callback()部分改成resolve()

举例3、AsyncParallelHook

异步并发 注册用的是tap、tapAsync或tapPromise 执行事件可以是callAsync或promise,不能用call 要实现的功能:

hook.tapAsync('tap1', (cb) => {
  console.log('tap1...');
  cb();
});
hook.tapPromise('tap2', () => {
  return Promise(resolve=>{
    console.log('tap2...');
    resolve();
  })
});
hook.intercept({
  tap: () => {},
  call: () => {},
});
await hook.promise();

需要执行的代码:

  • 调用拦截器的call方法
  • 调用拦截器的tap方法
  • 调用异步的tap方法
  • 统计是否执行完,是则完成
function anonymous(callback){
  // 1、定义变量
  var _taps = this.taps;
  var _x = _taps.map(tap => tap.fn);
  var _interceptors = this.interceptors;
  // 2、执行call钩子函数
  _interceptors.forEach(interceptor => {
    if (interceptor.call) {
      interceptor.call();
    }
  })
  // 3、依次执行taps
  const _interceptorsTaps = this.interceptors.map(_ => _.tap).filter(Boolean);
  // 需要执行的tap数量
  let count = _x.length;
  function done(){
    if (--count <= 0) {
      resolve();
    }
  }
  // 3.1 先执行tap拦截器函数
  _interceptorsTaps[0](tap);
  // 3.2 再执行tap函数
  _x[0](done);
  // 3.1 先执行tap拦截器函数
  _interceptorsTaps[1](tap);
  // 3.2 再执行tap函数
  _x[1]().then(done);
}

规律

  • 根据tap不同方法,构造不同的tap执行函数。
    • tap方法同步调用
    • tapAsync方法需要传入done函数(判断执行下一个还是结束)
    • tapPromise需要在then中处理done(判断执行下一个还是结束)
  • 根据call不同方法,使用不同函数体。
    • call方法同步调用,依次执行tap
    • callAsync需要依次调用tap,执行完调用callback
    • promise需要返回promise,taps执行完再resolve
// ①参数
function anonymous(args){
// ②变量

// ③拦截器call方法

// ④执行taps主体 
{
  // ⑤拦截器tap方法

  // ⑥执行tap

  // ⑦判断tap执行结果
}
// ⑧执行完taps
}
步骤callcallAsyncpromise
-callback参数-
--返回new Promise
-传入done函数then中调用done
-判断是否有下一个next函数,没有则callback结束判断是否有下一个next函数,没有则callback结束
步骤taptapAsynctapPromise
-调用cb结束tap返回promiseresolve()结束tap

tapable相关问题

1、为什么用new Function而不是直接依次执行代码?

2、constructor中的this.tap = this.tap作用?

3、为什么没有untap方法

总结

  • 文章是写完后再总结的,代码都是伪代码
  • 虽然是仿照的,但具体实现还是有些不一样,前面写Sync*Hook的基本一样,写到后面看了它的API和测试用例就直接自己写了,所以会有差异
  • 没有做异常处理
  • 每个hook都有测试用例,拷贝了部分tapable的测试用例,其中使用HookTester的部分没有拷贝
  • 如果有错,欢迎指出

参考