tapable

179 阅读9分钟

tapable

  • webpack基于tapable实现了事件流
  • 自定义plugin的时候,语法与tapable一致

语法

  • new tapable的某个基类,传入固定长度的array
  • 在实例上使用tap或tapAsync注册事件:
    • 第一个参数为name,给自己看的,内部不关心这个参数
    • callback,回调函数,接收参数可以小于等于初始化的array长度,超出了拿不到值,哪怕触发的时候传递了
  • call派发事件,事件依次执行,可以传递参数,执行顺序为先进先出

基类

  • Async开头为异步
    • 注册的话支持tap、tapAsync、tapPromise
    • 派发方法没有call,只有callAsync
  • Sync开头为同步
    • 注册方法为tap
    • 派发方法为call

按照值的返回分类

Basic

  • 依次执行每一个事件函数,不关心返回值
  • SyncHook、AsyncParallelHook、AsyncSeriesHook

Bail

  • 依次执行事件,当遇到返回值不为undefined的函数,后续函数不再执行
  • SyncBailHook、AsyncSeriesBailHook, AsyncParallelBailHook

Waterfall

  • 如果前一个事件返回不为undefined,那么将前一个事件的结果当做后一个事件的参数
  • SyncWaterfallHook,AsyncSeriesWaterfallHook

Loop

  • 不停的执行函数,知道所有函数返回为undefined(也可理解为没有返回值)
  • 遇到返回值不为undefined的函数,执行完当前函数,跳过后续函数,再次从起始位置依次执行函数
  • SyncLoopHook 和 AsyncSeriesLoopHook

SyncHook

  • 同步依次执行
const { SyncHook } = require("tapable");

const hook = new SyncHook(["name", "age"]);

hook.tap("1", (name, age, cc) => {
  console.log(1, name, age, cc); // 1 haha 10 undefined
});
hook.tap("2", (name, age) => {
  console.log(2, name, age);// 2 haha 10
});

hook.call("haha", 10);

SyncBailHook

  • 依次执行当前事件
    • 如果遇到函数有返回值,那么跳过后续函数
    • 如果执行完毕也没遇到返回值,也停止执行,不执行第二轮
const { SyncBailHook } = require("tapable");

const hook = new SyncBailHook(["name", "age"]);

hook.tap("1", (name, age) => {
  console.log(1, name, age);
  return 1;// 这里返回值了,所以后边的2不执行了
});
hook.tap("2", (name, age) => {
  console.log(2, name, age);
});

hook.call("haha", 10);

SyncLoopHooks

  • 当所有事件都返回undefind结束执行,否则重复执行
    • 如果当前函数返回值不为undefined,那么跳过后续函数,从头开始执行
    • 如果函数内部没有做好判断,会导致死循环
const { SyncLoopHook } = require("tapable");

const hook = new SyncLoopHook();

hook.tap("1", () => {
  console.log(1);
});
hook.tap("2", () => {
  console.log(2);
  return 1;
});
hook.tap("3", () => {
  console.log(3);
});

hook.call();

// 重复的121212
// 因为到2的时候不为undefined,重头开始了,永远不会走3

SyncWaterfallHook

  • 如果前面函数的值为undefined,那么使用以前的值
  • 否则使用前一个函数的返回值
  • 当出现不为undefined的返回值时,可以理解为全局的name就被覆盖了,那么依次执行的时候,会以最新的为准
const { SyncWaterfallHook } = require("tapable");

const hook = new SyncWaterfallHook(["name"]);

hook.tap("1", (name) => {
  console.log(1, name); // haha
  return 1;
});
hook.tap("2", (name) => {
  console.log(2, name); // 1
});
hook.tap("3", (name) => {
  console.log(3, name); // 1
});

hook.call("hh");

AsyncParallelHook

  • 异步触发
  • 依然是自上而下执行,结束会执行callAsync的callback
const { AsyncParallelHook } = require("tapable");

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

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

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

hook.tap("1", (name, age) => {
  console.log(1, name, age);
});
hook.tap("2", (name, age) => {
  console.log(2, name, age);
});
hook.tap("3", (name, age) => {
  console.log(3, name, age);
});
hook.tapAsync("4", (name, age, callback) => {
  console.log(4, name, age);
  callback();
});
hook.tapAsync("5", (name, age, callback) => {
  console.log(5, name, age);
  callback();
});
hook.tapPromise("6", (name, age) => {
  return new Promise((res, rej) => {
    console.log(6, name, age);
    res();
  });
});

hook.callAsync("cc", 11, () => {
  console.log("结束");
});


hook.callAsync("cc", 11, () => {
  console.log("结束");
});

AsyncParallelBailHook

  • 有一个回调返回不为undefined就结束
const { AsyncParallelBailHook } = require("tapable");
const hook = new AsyncParallelBailHook();

hook.tapPromise("1", () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 1000);
  });
});

hook.tapPromise("2", () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      //   resolve("result2");
    });
  });
});

hook.promise().then((res) => {
  console.log(res, 999);
});

AsyncSeriesHook

  • 依次执行
const { AsyncSeriesHook } = require("tapable");

const hook = new AsyncSeriesHook();

hook.tapAsync("1", (callback) => {
  console.log(1);
  callback();
});
hook.tapAsync("2", (callback) => {
  console.log(2);
  callback();
});

hook.callAsync((...arg) => {
  console.log("奥利给", arg);
});

AsyncSeriesBailHook

  • 当有返回值不为undefined,后续不走了
const { AsyncSeriesBailHook } = require("tapable");

const hook = new AsyncSeriesBailHook();

hook.tapAsync("1", (callback) => {
  console.log(1);
  callback(1); // 2不走了
});
hook.tapAsync("2", (callback) => {
  console.log(2);
  callback();
});

hook.callAsync((...arg) => {
  console.log("奥利给", arg);
});

事件的注册与派发

注册

tap

  • 同步注册

tapAsync

  • tapAsync为异步注册
  • 会额外给事件传递一个参数,callback
  • 执行callback会来标识当前回调执行完毕
const { AsyncParallelHook } = require("tapable");
const hook = new AsyncParallelHook(["name", "age"]);

hook.tapAsync("1", (name, age, callback) => {
  console.log(1, name, age);
  // callback可以放在异步里
  callback();
});

hook.callAsync("cc", 11, () => {
  console.log("结束");
});

tapPromise

  • 事件需要返回一个promise实例

派发

call

  • 同步派发

callAsync

  • 异步派发,可多传递一个callback,当所有函数执行完毕,会执行callback
    • 前提是函数告知我们,如tapAsync中执行callback,promise执行resolve
    • 给callback传参会立即结束后续执行

promise

  • 就是promise,可以理解为promise.all
hook
  .promise("cc", 11)
  .then((res) => {
    console.log("啥呀");
  })
  .catch((err) => {
    console.log(err);
  })
  .finally(() => {
    console.log(".....");
  });

拦截器 intercept

  • intercept接收一个object,每当对应的钩子触发,会进行拦截,可以有多个intercept
  • call
    • call之前触发
  • tap
    • 并非是tap执行,而是tap注册的事件被执行前触发
  • register
    • 注册事件的时候会执行:tap、tapAsync、tapPromise
const { SyncHook } = require("tapable");
const hook = new SyncHook(["name", "age"]);
hook.intercept({
  register(tapInfo) {
    console.log("register拦截器1", tapInfo);
  },
  call(name, age) {
    console.log("call拦截器1", name, age);
  },
  tap(tapInfo) {
    console.log("tap拦截器1", tapInfo);
  },
});
hook.intercept({
  register(tapInfo) {
    console.log("register拦截器2", tapInfo);
  },
  call(name, age) {
    console.log("call拦截器2", name, age);
  },
  tap(tapInfo) {
    console.log("tap拦截器2", tapInfo);
  },
});

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

hook.call("cc", 11);

stage

  • 设置权重,某些事件优先级比较高,但设置的时候相对靠后,我们希望它按照给定顺序执行
  • stage写在注册是第一个参数中:
    • 数值越大,级别越高,越先执行
    • 同级别按照注册时间执行
    • 默认为0
  • 在注册事件的时候,会按照stage进行插入,而不是call的时候排序
const { SyncHook } = require("tapable");
const hook = new SyncHook(["name", "age"]);

hook.tap({ name: "tap1", stage: 2 }, () => {
  console.log("tap2");
});
hook.tap({ name: "tap1", stage: 1 }, () => {
  console.log("tap1");
});

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

hook.call("cc", 11);
// 1 tap1 tap2

before

  • 以名字来匹配优先级,高于stage
  • 可以指定在一个或多个前面执行
    • 多个的话,会以最前面的执行
    • 如果指定的项不存在,那么会直接到第一项
  • 同样是在注册的时候进行排序
const { SyncHook } = require("tapable");

const hook = new SyncHook();
hook.tap("tap1", () => {
  console.log("tap1");
});
hook.tap("tap2", () => {
  console.log("tap2");
});
hook.tap({ name: "tap3" }, () => {
  console.log("tap3");
});
hook.tap({ name: "tap4", before: ["tap3"] }, () => {
  console.log("tap4");
});
hook.tap({ name: "tap5", before: ["tap2", "tap6"] }, () => {
  // 6不存在,那么5在第一项
  console.log("tap5");
});

hook.call();// 5 1 2 4 3

HookMap

  • 通过HookMap将基类进行分组工厂模式
    • for方法设置一个基类
    • get方法获取一个基类
    • 触发还是call,设置还是tap
  • 原理为初始化的时候,保存callback
    • for的本质还是get,但是会加一层初始化逻辑,如果key不存在就执行callback初始化
const { SyncHook, HookMap } = require("tapable");
const map = new HookMap(() => new SyncHook(["name"]));

map.for("key1").tap("k1-p1", (name) => {
  console.log("k1-p1", name);
});
map.for("key1").tap("k1-p2", (name) => {
  console.log("k1-p2", name);
});
map.for("key2").tap("k2-p1", (name) => {
  console.log("k2-p1", name);
});
map.for("key3").tap("k3-p1", (name) => {
  console.log("k3-p1", name);
});

map.get("key1").call("name");// 触发key1
map.get("key2").call("name");// 触发key2
map.get("key3").call("name");// 触发key3

实现

  • 首先这么多类都是基于Hook的,在Hook上方了一些公有方法,可以继承
  • 一些私有方法写在私有类本身,因为this指向实例,可以向上查找,所以完全没问题
  • 整体使用的是一个懒编译模式,不调用派发不生成函数,以call举例,并非是for循环执行,而是通过new Function生成了一个新的函数,在里面依次执行
    • 如果不调用call的话,不加载函数
    • 如果调用的话,生成一次后覆盖call,那么后续执行call的话,执行的就是前面生成的fn,不用走编译阶段了
    • 如果调用后,又进行事件的注册,那么会先清除上次的函数,后续调用call会重新生成,避免注册后不执行问题

index.js

  • 收集并导出
const SyncHook = require("./SyncHook");
const AsyncParallelHook = require("./AsyncParallelHook");
const HookMap = require("./HookMap");
module.exports = {
  SyncHook,
  AsyncParallelHook,
  HookMap,
};

Hook

// 以CALL_DELEGATE为例,第一次会走CALL_DELEGATE,后续直接走`生成的函数`了,不会再进入CALL_DELEGATE函数
// 首次执行创建函数逻辑,然后this.call执行会返回函数,但由于是闭包,所以当前函数会有局限性
// 后续执行都是直接执行的call返回的函数
// 所以我们需要清除下
const CALL_DELEGATE = function (...args) {
  // 动态创建一个sync类型的call方法,先赋值给this,改变this指向
  this.call = this._createCall("sync");
  // 再重新赋值为返回的函数
  return this.call(...args);
};
const CALL_ASYNC_DELEGATE = function (...args) {
  this.callAsync = this._createCall("async");
  return this.callAsync(...args);
};
const PROMISE_DELEGATE = function (...args) {
  this.promise = this._createCall("promise");
  return this.promise(...args);
};

class Hook {
  constructor(args) {
    this.args = Array.isArray(args) ? args : []; // 回调函数的形参列表
    this.taps = []; // 回调函数obj的
    this.call = CALL_DELEGATE; // 代理的call
    this.callAsync = CALL_ASYNC_DELEGATE; // 代理的callAsync
    this.promise = PROMISE_DELEGATE;
    this._x; // 默认为undefined,后续会变为[],存储回调函数
    this.interceptors = []; // 拦截器数组,可保存多个拦截器
  }
  // 注册拦截器
  intercept(interceptor) {
    this.interceptors.push(interceptor);
  }
  tap(options, fn) {
    // 如果是通过tap注册的回调,那么类型type=sync,表示fn要以同步的方式调用
    this._tap("sync", options, fn);
  }
  tapAsync(options, fn) {
    this._tap("async", options, fn);
  }
  tapPromise(options, fn) {
    this._tap("promise", options, fn);
  }
  _tap(type, options, fn) {
    if (typeof options === "string") {
      // 兼容string,如果是string就拼接成object
      options = { name: options };
    }
    // 整合参数
    const tapInfo = { ...options, type, fn };
    // 注册前执行拦截钩子
    this.runRegisterInterceptors(tapInfo);
    // 注册
    this._insert(tapInfo);
  }
  runRegisterInterceptors(tapInfo) {
    for (const interceptor of this.interceptors) {
      if (interceptor.register) {
        // 注册前置钩子
        interceptor.register(tapInfo);
      }
    }
  }
  // 注册
  _insert(tapInfo) {
    // 每次注册都重新设置一次,避免执行派发后又注册事件,再派发产生的丢失问题
    this.resetCompilation();
    let before;
    if (typeof tapInfo.before === "string") {
      before = new Set([tapInfo.before]);
    } else if (Array.isArray(tapInfo.before)) {
      before = new Set(tapInfo.before);
    }
    // 按照stage顺序进行后续插入
    let stage = 0;
    if (typeof tapInfo.stage === "number") {
      stage = tapInfo.stage;
    }
    let i = this.taps.length;
    while (i > 0) {
      // 每次--,首次为lendth,length比index大1,所以--为末尾项
      i--;
      // 每次向后移一位
      const x = this.taps[i];
      this.taps[i + 1] = x;
      if (before) {
        if (before.has(x.name)) {
          // 因为是从后往前找的,所以每超过一个,就在set中删掉
          before.delete(x.name);
          continue;
        }
        if (before.size > 0) {
          // 如果当前项不存在before中,且before还有,跳过当前遍历
          continue;
        }
      }
      const xStage = x.stage || 0;
      if (xStage > stage) {
        // 如果当前的值比要插入的值大,继续向前查找
        continue;
      }
      i++;
      break;
    }
    this.taps[i] = tapInfo;
    // this.taps.push(tapInfo);
  }
  // 还原,防止事件丢失
  resetCompilation() {
    this.call = CALL_DELEGATE;
    this.callAsync = CALL_ASYNC_DELEGATE;
    this.promise = PROMISE_DELEGATE;
  }
  _createCall(type) {
    return this.compile({
      taps: this.taps,
      args: this.args,
      interceptors: this.interceptors,
      type,
    });
  }
}
module.exports = Hook;

HookCodeFactory

class HookCodeFactory {
  setup(hookInstance, options) {
    hookInstance._x = options.taps.map((tapInfo) => tapInfo.fn);
  }
  init(options) {
    this.options = options;
  }
  // 获取初始化形参集合
  args(config = {}) {
    const { before, after } = config;
    let allArgs = this.options.args || [];
    allArgs = [...allArgs];
    if (before) {
      allArgs.unshift(before);
    }
    if (after) {
      allArgs.push(after);
    }
    if (allArgs.length > 0) {
      return allArgs.join(",");
    }
  }
  header() {
    let code = ``;
    code += `var _x = this._x;\n`;
    const interceptors = this.options.interceptors;
    if (interceptors.length > 0) {
      code += `var _taps = this.taps;\n`;
      code += `var _interceptors = this.interceptors;\n`;
      interceptors.forEach((interceptor, i) => {
        if (interceptor.call) {
          code += `_interceptors[${i}].call(${this.args()});\n`;
        }
      });
    }
    return code;
  }
  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(
          this.args({ after: "_callback" }),
          this.header() + this.content({ onDone: () => "_callback();\n" })
        );
        break;
      case "promise":
        let tapsContent = this.content({ onDone: () => "_resolve();\n" });
        let content = `return new Promise(function (_resolve, _reject){
          ${tapsContent}
        })`;
        fn = new Function(this.args(), this.header() + content);
        break;
    }
    this.deinit();
    return fn;
  }
  deinit() {
    this.options = null;
  }
  callTapsSeries() {
    let code = "";
    for (let j = 0; j < this.options.taps.length; j++) {
      const tapContent = this.callTap(j, {});
      code += tapContent;
    }
    return code;
  }
  callTapsParallel({ onDone }) {
    let code = ``;
    const taps = this.options.taps;
    code += `var _counter = ${taps.length};\n`;
    code += `
      var _done = function() {
        ${onDone()};
      };\n
    `;
    for (let i = 0; i < taps.length; i++) {
      const tapContent = this.callTap(i, {
        onDone: () => `if(--_counter === 0)_done();`,
      });
      code += tapContent;
    }
    return code;
  }

  callTap(tapIndex, { onDone }) {
    let code = ``;
    const interceptors = this.options.interceptors;
    if (interceptors.length > 0) {
      code += `var _tap${tapIndex} = _taps[${tapIndex}];\n`;
      interceptors.forEach((interceptor, index) => {
        if (interceptor.tap) {
          code += `_interceptors[${index}].tap(_tap${tapIndex});\n`;
        }
      });
    }
    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 += onDone();
        break;
      case "async":
        code += `_fn${tapIndex}(${this.args()}, function() {
          if (--_counter === 0) _done();
        });\n`;
        break;
      case "promise":
        code += `
        var _promise${tapIndex} = _fn${tapIndex}(${this.args()});
        _promise${tapIndex}.then(function(){
          if(--_counter === 0) _done();
        }).catch(err=>{
          _reject(err);
        })
        `;
        break;
    }
    return code;
  }
}
module.exports = HookCodeFactory;

AsyncParallelHook

const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class AsyncParallelHookCodeFactory extends HookCodeFactory {
  content({ onDone }) {
    return this.callTapsParallel({ onDone });
  }
}

const factory = new AsyncParallelHookCodeFactory();

class AsyncParallelHook extends Hook {
  compile(options) {
    factory.setup(this, options);
    return factory.create(options);
  }
}

module.exports = AsyncParallelHook;

SyncHook

const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
class SyncHookCodeFactory extends HookCodeFactory {
  content() {
    return this.callTapsSeries();
  }
}
const factory = new SyncHookCodeFactory();
class SyncHook extends Hook {
  compile(options) {
    factory.setup(this, options);
    return factory.create(options);
  }
}
module.exports = SyncHook;

HookMap

class HookMap {
  constructor(factory) {
    this.factory = factory;
    this.map = new Map();
  }
  get(key) {
    return this.map.get(key);
  }
  for(key) {
    const hook = this.get(key);
    if (hook) return hook;
    let newHook = this.factory();
    this.map.set(key, newHook);
    return newHook;
  }
}
module.exports = HookMap;