认识与实现tapable异步钩子

前言

前面,我们通过简单认识tapablejs如何根据需求动态生成函数了解到了tapable的钩子类型以及同步钩子的具体实现。那么本文就来讲讲异步钩子。

异步钩子的简单使用

异步钩子的注册方式共三种,其中一种大家都很熟悉了,那就是tap,另外两种分别是tapPromise和tapAsync。

基本异步钩子都如此,所以本文就以AsyncParallelHook为例,从简单使用AsyncParallelHook到实现AsyncParallelHook。

tapPromise

tapPromise方式注册,和tap方式注册一样的接收两个参数,第一个参数算是个key值,第二个参数是执行函数。但tapPromise对执行函数有一定的要求,那就是要返回一个promise。

  • 正确的写法:
js 复制代码
const { AsyncParallelHook } = require('tapable');

const hook = new AsyncParallelHook(['name', 'age']);

hook.tapPromise('1', (name, age) => {
  return new Promise(resolve => {
    console.log('1', name, age);
    resolve(1);
  });
})
  • 错误的写法:
js 复制代码
const { AsyncParallelHook } = require('tapable');

const hook = new AsyncParallelHook(['name', 'age']);

hook.tapPromise('1', (name, age) => {
  console.log('1', name, age);
})

这种错误的写法执行时就会报错,要求我们必须返回一个promise。

由于返回的是promise,所以他们的调用方式自然也是promise的方式。我们来看完整的例子。

js 复制代码
const { AsyncParallelHook } = require('tapable');

const hook = new AsyncParallelHook(['name', 'age']);

hook.tapPromise('1', (name, age) => {
  return new Promise(resolve => {
    console.log('1', name, age);
    resolve(1);
  });
})
hook.tapPromise('2', (name, age) => {
  return new Promise(resolve => {
    console.log('2', name, age);
    resolve(2);
  });
})
hook.tapPromise('3', (name, age) => {
  return new Promise(resolve => {
    console.log('3', name, age);
    resolve(3);
  });
})

hook.promise('Rippi', 18).then(() => {
  console.log('done');
});

运行结果:

tapAsync

tapAsync方式注册,同样也是接受两个参数,但有所不同的是,第二个执行函数除了默认传了我们定义的参数外,还会多接收一个callback参数,需要我们手动执行callback,这个callback就像是promise中的resolve一样,告诉tapable,这个注册的函数执行结束了。

同样的,类似上面的promise一样,tapAsync也有自己的调用方式,名为callAsync。

话不多说,直接看完整例子。

js 复制代码
const { AsyncParallelHook } = require('tapable');

const hook = new AsyncParallelHook(['name', 'age']);

console.time('cost');
// 注册 异步有三种注册方式 tap tapAsync promise
hook.tapAsync('1', (name, age, callback) => {
  setTimeout(() => {
    console.log('1', name, age);
    callback();
  }, 1000);
});
hook.tapAsync('2', (name, age, callback) => {
  setTimeout(() => {
    console.log('2', name, age);
    callback();
  }, 2000);
});
hook.tapAsync('3', (name, age, callback) => {
  setTimeout(() => {
    console.log('3', name, age);
    callback();
  }, 3000);
});
// 异步钩子没有call,但有callAsync promise
hook.callAsync('Rippi', 18, () => {
  console.log('done');
  console.timeEnd('cost');
});

执行结果:

混合方式注册

异步钩子和同步钩子不同,它是有三种注册方式的,所以,自然的就允许我们乱来,三种注册方式混合着用。

我们先看代码。

js 复制代码
const { AsyncParallelHook } = require('tapable');

const hook = new AsyncParallelHook(['name', 'age']);

console.time('cost');

hook.tap('1', (name, age) => {
  console.log('1', name, age);
})

hook.tapAsync('2', (name, age, callback) => {
  setTimeout(() => {
    console.log('2', name, age);
    callback();
  }, 2000);
});
hook.tapPromise('3', (name, age) => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('3', name, age);
      resolve(3);
    }, 3000);
  });
})

hook.promise('Rippi', 18).then(() => {
  console.log('done');
  console.timeEnd('cost');
});

看着三种方式注册好像很乱,其实不然。

  • tap方式注册的,自然是直接执行,执行完就直接结束
  • tapAsync执行后需要等callback调用完毕后才结束。
  • tapPromise执行后需要等resolve调用完毕后才结束。

执行结果:

实现

注册方式我们都了解了,那么我们来实现下吧。

通过上一篇js如何根据需求动态生成函数文章,我们了解到其本质就是拼接字符串,而模仿实现的方法也很简单,那就是通过调试,获取到最后生成的代码,然后进行总结,最后再是根据总结出来的内容转化成代码实现。

AsyncParallelHook实现

同理,根据上面tapAsync注册方式的例子,我们先进行调试,看下最后生成的动态函数长什么样。

js 复制代码
function anonymous(name, age, _callback) {
  var _x = this._x;
  var _counter = 3;
  var _done = function () {
    _callback();
  };
  var _fn0 = _x[0];
  _fn0(name, age, function () {
    if (--_counter === 0) _done();
  });
  var _fn1 = _x[1];
  _fn1(name, age, function () {
    if (--_counter === 0) _done();
  });
  var _fn2 = _x[2];
  _fn2(name, age, function () {
    if (--_counter === 0) _done();
  });
}

看了上面的代码是否恍然大悟,原来callback是这样一回事,每次调用调用callback都会进行计数器的减一,当计数器减到0后,说明我们的所有注册函数都执行完了,然后执行我们callAsync里的函数。

接着,我们对比下tap生成的函数。

js 复制代码
function anonymous(name, age) {
  var _x = this._x;
  // 注册的方法1
  var _fn0 = _x[0];
  _fn0(name, age);
  // 注册的方法1
  var _fn1 = _x[1];
  _fn1(name, age);
  // 注册的方法1
  var _fn2 = _x[2];
  _fn2(name, age);
}

对比过后,发现三个部分(参数部分、头部、内容部分)除了头部,均有所差别,那么我们继续在之前实现的代码中进行一定的判断就好了。

参数部分

参数部分我们需要加上一个判断,如果是tapAsync方式注册的,需要多一个callback的参数,这里我们把这个参数称之为after

js 复制代码
args(config = {}) {
  const { after } = config;
  let allArgs = [...this.options.args];
  if (after) {
    allArgs = [...allArgs, after];
  }
  return allArgs.join(',');
}

由于args方法增加了判断,那么我们在create时也需要进行判断,针对不同情况给args传参。

js 复制代码
create(options) {
    this.init(options);
    let fn;
    switch (options.type) {
      case 'sync':
        fn = new Function(
          this.args(),
          this.header() + this.content(),
        );
        break;
      case 'async':
        fn = new Function(
          // 此处传了after
          this.args({ after: '_callback' }),
          this.header() + this.content({ onDone: '_callback();' }),
        );
        break;
    }
    this.deInit();
    return fn;
  }

内容部分

内容部分,顶部需要添加一个计数器以及一个done函数。

js 复制代码
callTapsParallel({ onDone }) {
    const taps = this.options.taps;
    let code = '';
    // 添加计数器
    code += `var _counter = ${taps.length};\n`;
    // 定义done函数
    code += `var _done = function() {
      ${onDone}
    };\n`;
    for (let j = 0; j < taps.length; j++) {
      const tapContent = this.callTap(j, true);
      code += tapContent;
    }
    return code;
  }
}

callTap(tapIndex, onDone) {
  let code = '';
  // 取出回调函数
  code += `var _fn${tapIndex} = _x[${tapIndex}];\n`;
  let tapInfo = this.options.taps[tapIndex];
  // 调用回调函数
  switch (tapInfo.type) {
    case 'sync':
      code += `_fn${tapIndex}(${this.args()});\n`;
      if (onDone) code += `if (--_counter === 0) _done();\n`;
      break;
    case 'async':
      // 每一个注册函数生成的函数体
      code += `_fn${tapIndex}(${this.args()}, function() {
        if (--_counter === 0) _done();
      });\n`;
      break;
  }
  return code;
}

最后我们创建一个AsyncParallelHook文件,完成AsyncParallelHook类,让它调用工厂类中的callTapsParallel方法即可。

js 复制代码
const Hook = require('./Hook');
const HookCodeFactory = require('./HookCodeFactory');
class AsyncParallelHookCodeFactory extends HookCodeFactory {
  content(options) {
    return this.callTapsParallel(options);
  }
}
const factory = new AsyncParallelHookCodeFactory();

class AsyncParallelHook extends Hook {
  compile(options) {
    // 初始化代码工厂
    factory.setup(this, options);
    return factory.create(options);
  }
}

module.exports = AsyncParallelHook;

到此为止,AsyncParallelHook我们就基本实现了,剩下的tapPromise注册方法,交由各位自行实现吧,只要理解了原理是拼接字符串,其实实现起来都非常简单的。

文末会附上代码仓库(包含了tapPromise的实现)。

结尾

本文通过AsyncParallelHook的两个例子认识异步钩子以及异步钩子的注册方式,最后再手动实现了AsyncParallelHook的tapAsync注册方式。基本tapable的钩子和其基本实现原理都覆盖到了,tapable剩余的钩子都大差不差,想要更深入的了解,可以进行甚多的demo书写以及源码查阅。

代码仓库

最后,希望大家都能有所收获,也希望看到此处的大家能给个小赞🌹

相关推荐
aPurpleBerry15 分钟前
JS常用数组方法 reduce filter find forEach
javascript
GIS程序媛—椰子43 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0011 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x1 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
木舟10091 小时前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤43911 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安2 小时前
前端第二次作业
前端·css·css3
啦啦右一2 小时前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习