【Webpack】一文带你吃透 Tapable 中的 tap 与 call
前言
本文通过由浅入深的例子,从源码层面带你了解一下 Tapable 中的 tap 与 call,也就是如何进行事件的注册与触发。
Tapable
-
tapable
是webpack
控制事件流的超级管家; -
这个小型库是
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 函数,但是他们可以并行运行)
除了 Sync
和 Async
我们可以看到上面的 Hook
还带了其他关键字:Basic、Waterfall、Bail、Loop
- Basic hook(没有 Waterfall、Bail、Loop 在它的名字里面):所有 tapped 的事件依次执行,互不干扰
- Waterfall:与 Basic hook 不同的是它将返回值从每个函数传递到下一个函数
- Bail:当任何 tapped 函数返回了任何值,bail hook 将停止执行其余的函数
- 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
-
那我们上面讲的
tap
和call
呢,不急的,慢慢来,既然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 源码地址
转载自:https://juejin.cn/post/7158608014545518599