像素

tapable学习

基本使用

Tapable是一个类似于 Node.js 中的 EventEmitter 的库,但它更专注于自定义事件的触发和处理。

const {SyncHook} = require("tapable"); //同步钩子

//第一步:实例化钩子函数,定义形参
const syncHook = new SyncHook(["name", "age"]);

//第二步:注册事件1
syncHook.tap("监听器1", (name, age) => {
  console.log("监听器1:", name, age);
});

//第二步:注册事件2
syncHook.tap("监听器2", (name) => {
  console.log("监听器2", name);
});

//第三步:注册事件3
syncHook.tap("监听器3", (name) => {
  console.log("监听器3", name);
});
//第三步:触发事件,这里传的是实参,会被每一个注册函数接收到
syncHook.call("ll", "20");
// 监听器1: ll 20
// 监听器2 ll
// 监听器3 ll

采用的是发布订阅模式,通过 tap 函数注册监听函数,然后通过 call 函数按顺序执行之前注册的函数。

const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");

hooks

同步钩子:tap 方法是唯一注册事件的方法,通过 call 方法触发同步钩子的执行。 异步钩子:可以通过 tap、tapAsync、tapPromise三种方式来注册,通过对应的 callAsync、promise 这两种方式来触发注册的函数。 异步钩子分为两类:

  • 异步串行钩子( AsyncSeries ):可以被串联(连续按照顺序调用)执行的异步钩子函数。
  • 异步并行钩子( AsyncParallel ):可以被并联(并发调用)执行的异步钩子函数。

hooks执行机制

  • Basic Hook : 基本类型的钩子,执行每一个注册的事件函数,并不关心每个被调用的事件函数返回值如何。
  • Waterfall : 瀑布类型的钩子,如果前一个事件函数的结果 result !== undefined,则 result 会作为后一个事件函数的第一个参数。
  • Bail : 保险类型钩子,执行每一个事件函数,遇到第一个结果 result !== undefined 则返回,不再继续执行
  • Loop : 循环类型钩子,不停的循环执行事件函数,直到所有函数结果 result === undefined

WaterfallHook 示例

const { SyncWaterfallHook } = require("tapable");

const hook = new SyncWaterfallHook(["author", "age"]); 

//通过tap函数注册事件
hook.tap("测试1", (param1, param2) => {
  console.log("测试1接收的参数:", param1, param2);
});

hook.tap("测试2", (param1, param2) => {
  console.log("测试2接收的参数:", param1, param2);
  return "123";
});

hook.tap("测试3", (param1, param2) => {
  console.log("测试3接收的参数:", param1, param2);
});

//通过call方法触发事件
hook.call("ll", "20");
// 测试1接收的参数: ll 20
// 测试2接收的参数: ll 20
// 测试3接收的参数: 123 20

异步hook

const { AsyncParallelHook } = require("tapable");

const hook = new AsyncParallelHook(["author", "age"]); 

console.time("time");
// 异步钩子需要通过tapAsync函数注册事件,同时也会多一个callback参数,
// 执行callback告诉hook该注册事件已经执行完成
hook.tapAsync("测试1", (param1, param2, callback) => {
  setTimeout(() => {
    console.log("测试1接收的参数:", param1, param2);
    callback();
  }, 2000);
});

hook.tapAsync("测试2", (param1, param2, callback) => {
  console.log("测试2接收的参数:", param1, param2);
  callback();
});

hook.tapAsync("测试3", (param1, param2, callback) => {
  console.log("测试3接收的参数:", param1, param2);
  callback();
});

//call方法只有同步钩子才有,异步钩子得使用callAsync
hook.callAsync("ll", "20", (err, result) => {
  //等全部都完成了才会走到这里来
  console.log("这是成功后的回调", err, result);
  console.timeEnd("time");
});

// 测试2接收的参数: ll 20
// 测试3接收的参数: ll 20
// 测试1接收的参数: ll 20
// 这是成功后的回调 undefined undefined
// time: 2.005s

核心思想

参照 SyncHook 钩子的使用

  1. 实例化 hook
  2. 通过 tap 函数添加订阅
  3. 调用 call 函数触发事件

tap:将监听信息组装存放在 this.taps 中

this.taps = [
  {
    name: "监听器1",
    type: "sync",
    fn: (param1, param2) => {
      console.log("监听器1接收参数:", name, age);
    },
  },
]; //用来存放监听

/*
* tap
*/
this.tap(option, fn) {
  //如果传入的是字符串,包装成对象
  if (typeof option === "string") {
    option = { name: option };
  }
  // type 确定执行类型
  const tapInfo = { ...option, type: "sync", fn };
  this.taps.push(tapInfo);
}

call:按照指定的类型去执行 this.tabs 中订阅信息的 fn,先动态生成执行代码,再执行生成的代码

源码中通过 compile 创建执行函数

const CALL_DELEGATE = function(...args) {
  // 获取执行函数
    this.call = this._createCall("sync");
    return this.call(...args); // 执行
};

this.call = CALL_DELEGATE;

_createCall(type) {
  return this.compile({
    taps: this.taps,
    interceptors: this.interceptors,
    args: this._args,
    type: type
  });

// 通过 this.compile 生成执行函数

compile 简单实现

class SyncHook {
  //省略其他

  compile({ args, taps, type }) {
    const getHeader = () => {
      let code = "";
      code += `var taps=this.taps;\n`;
      return code;
    };

    const getContent = () => {
      let code = "";
      for (let i = 0; i < taps.length; i++) {
        code += `var fn${i}=taps[${i}].fn;\n`;
        code += `fn${i}(${args.join(",")});\n`;
      }
      return code;
    };
    // 利用 Function 创建返回执行函数
    return new Function(args.join(","), getHeader() + getContent());
  }
}

compile 中通过 new Function 创建执行函数,也就是每次执行call的时候 才会生成执行函数,为了性能考虑。相较于 forEach 会更快一些。

参考

Is the new Function performance really good? · Issue #162 · webpack/tapable