likes
comments
collection
share

【Webpack】一文带你吃透 Tapable 中的 tap 与 call

作者站长头像
站长
· 阅读数 32

前言

本文通过由浅入深的例子,从源码层面带你了解一下 Tapable 中的 tap 与 call,也就是如何进行事件的注册与触发。

Tapable

  • tapablewebpack 控制事件流的超级管家;

  • 这个小型库是 webpack 的一个核心工具,但也可用于其他地方, 以提供类似的插件接口。

  • 在 webpack 中的许多对象都扩展自 Tapable 类。

  • 它对外暴露了 tap,tapAsync 和 tapPromise 等方法, 插件可以使用这些方法向 webpack 中注入自定义构建的步骤,这些步骤将在构建过程中触发。

  • git 源码地址:github.com/webpack/tap…

钩子函数

  • Tapable 使用前,先写个简单小栗子(最近可以吃糖炒栗子了哦,很甜的哦),帮助理解 Tapable 的使用
class MyHook {
  hooks = {
    onBeforeCreated: [],
    onCreated: [],
  };
  onHooks(name, callback) {
    if (this.hooks[name]) {
      this.hooks[name].push(callback);
    }
  }
  created() {
    this.hooks.onBeforeCreated.forEach((callback) => {
      callback();
    });
    this.hooks.onCreated.forEach((callback) => {
      callback();
    });
  }
}

const hook = new MyHook();
// 注册钩子
hook.onHooks('onBeforeCreated', () => {
  console.log('onBeforeCreated');
});
hook.onHooks('onCreated', () => {
  console.log('onCreated');
});
// 触发
hook.created();

Tapable 的类和上面的思想是很类似的,我们先从简单的代码入手

Tapable 提供的 hook 类:

  • SyncHook
  • SyncBailHook
  • SyncWaterfallHook
  • SyncLoopHook
  • AsyncParallelHook
  • AsyncParallelBailHook
  • AsyncSeriesHook
  • AsyncSeriesBailHook
  • AsyncSeriesWaterfallHook

同步和异步:

  • 同步 hook 有:SyncHook、SyncBailHook、SyncWaterfallHook、SyncLoopHook
  • 异步 hook:
    • 异步串行:AsyncSeriesHook、AsyncSeriesBailHook、AsyncSeriesWaterfallHook(一个异步串行 hook 可以注册 synchronous、callback-based、promise-based 函数,依次执行)
    • 异步并行:AsyncParallelHook、AsyncParallelBailHook(一个异步并行 hook 也是可以注册 synchronous、callback-based、promise-based 函数,但是他们可以并行运行)

除了 SyncAsync 我们可以看到上面的 Hook 还带了其他关键字:Basic、Waterfall、Bail、Loop

  1. Basic hook(没有 Waterfall、Bail、Loop 在它的名字里面):所有 tapped 的事件依次执行,互不干扰
  2. Waterfall:与 Basic hook 不同的是它将返回值从每个函数传递到下一个函数
  3. Bail:当任何 tapped 函数返回了任何值,bail hook 将停止执行其余的函数
  4. Loop:若任一事件的返回值不为 undefined,则该事件链再次从头开始执行,直到所有 handler 均返回 undefined

hook 类对外提供的方法

tap tapAsync tapPromise call callAsync promise 等,本文着重讲一下注册与触发

  • tap、tapAsync、tapPromise 用于注册事件
  • call、callAsync、promise 用于触发事件

以 Tapable 中的 SyncHook 类为例,如何使用

tap 用于注册事件,call 用于触发事件

const { SyncHook } = require('tapable');
// 实例化同步钩子
const syncHook = new SyncHook(['name']);
// 注册事件
syncHook.tap('A', (name) => {
  console.log('A:', name);
});
syncHook.tap('B', (name) => {
  console.log('B:', name);
});
// 触发事件
syncHook.call('breeze');
// 输出结果
// A: breeze
// B: breeze

看整个流程是不是和上面钩子的思想很类似的,接下来我们看看 Tapable 的源码是怎么写的吧

Tapable 源码(SyncHook 为例)

SyncHook

虽然 Tapable 提供了很多的 hook 类,但是每个类都继承自基类 HookCodeFactory,初始化过程也是类似的

  • 所以呢,依旧 SyncHook 为栗子,SyncHook 全部源码如下,很简短
const Hook = require('./Hook');
const HookCodeFactory = require('./HookCodeFactory');

// 工厂类的作用是生成不同的compile方法
class SyncHookCodeFactory extends HookCodeFactory {
  content({ onError, onDone, rethrowIfPossible }) {
    return this.callTapsSeries({
      onError: (i, err) => onError(err),
      onDone,
      rethrowIfPossible,
    });
  }
}

const factory = new SyncHookCodeFactory();

// 替换 Hook 基类中的 tapAsync 方法,Sync 同步钩子禁止以 tapAsync 的方式调用
const TAP_ASYNC = () => {
  throw new Error('tapAsync is not supported on a SyncHook');
};
// 同上覆盖
const TAP_PROMISE = () => {
  throw new Error('tapPromise is not supported on a SyncHook');
};
// 每个类型 hook 都要实现 compile,需要调用各自的工厂函数来生成钩子的 call 方法,下文讲到 call 时会讲到
const COMPILE = function (options) {
  factory.setup(this, options);
  return factory.create(options);
};

// 构造函数
function SyncHook(args = [], name = undefined) {
  const hook = new Hook(args, name); // 似乎关键信息还是在基类里面
  hook.constructor = SyncHook; // 将构造函数指向自己
  hook.tapAsync = TAP_ASYNC; // 覆盖
  hook.tapPromise = TAP_PROMISE; // 覆盖
  hook.compile = COMPILE;
  return hook;
}

SyncHook.prototype = null;

module.exports = SyncHook;
tap
  • 那我们上面讲的 tapcall 呢,不急的,慢慢来,既然 SyncHook 中没有其代码,自然就在 Hook 基类中了

  • 首先 tap 部分,以下为精简代码,我们这边只需要关注 tap 部分的代码,重在流程

class Hook {
  constructor(args = [], name = undefined) {
    this.taps = [];
    this.tap = this.tap;
  }
  tap(options, fn) {
    this._tap('sync', options, fn);
  }
  _tap(type, options, fn) {
    this._insert(options);
  }
  _insert(item) {
    this._resetCompilation(); // 每次注册事件时,将 call 重置,需要重新编译生成 call 方法
    let before;
    if (typeof item.before === 'string') {
      before = new Set([item.before]);
    } else if (Array.isArray(item.before)) {
      before = new Set(item.before);
    }
    let stage = 0;
    if (typeof item.stage === 'number') {
      stage = item.stage;
    }
    let i = this.taps.length;
    // 通过 before 和 stage 参数来调整 taps 数组的排序
    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; // taps 暂存所有注册的回调函数
  }
}
  • 通过以上代码我们可以看到,_insert 方法将回调函数全部注册到了 taps 数组中
call
  • 说完 tap,我们接着来看下 call 方法,tap 用来注册事件,call 则是用来触发事件
const CALL_DELEGATE = function (...args) {
  // 只会在第一次执行 call 的时候执行,依据钩子类型和回调数组生成真实执行的函数。(此处实现此功能,利用了惰性函数)
  // 并重新赋值给 this.call
  // 第二次执行的时候直接执行,不重复走这边的流程
  this.call = this._createCall('sync');
  return this.call(...args);
};

class Hook {
  constructor(args = [], name = undefined) {
    this._call = CALL_DELEGATE;
    this.call = CALL_DELEGATE;
  }

  // 在继承基类 Hook 的类中,各自都会实现 compile,不需要使用这边的,应该要被覆盖的
  compile(options) {
    throw new Error('Abstract: should be overridden');
  }

  // 第一次执行 call 或 call 被重置的时候需要调用 各自类中的 compile 方法去生成 call 方法
  _createCall(type) {
    return this.compile({
      taps: this.taps,
      interceptors: this.interceptors,
      args: this._args,
      type: type,
    });
  }
}

注意:在源码中用于注册事件有三种方法:tap、tapAsync、tapPromise,对应的用于触发事件的三种:call、callAsync、promise

后续

  • 如果想要了解其他 Hook 或者方法的用法,可以去看对应文件的源码,使用相同思路去解读源码即可。
  • Tapable 源码地址