Tapable是什么
Tapable是Webpack的核心模块之一, 基于发布订阅模式, 提供了一套钩子(Hook)机制, 用于控制webpack的事件流和插件系统. 通过Tapable,插件可以在 webpack 构建过程中的特定时机注册并执行自定义逻辑,从而实现灵活的流程控制。
核心概念
- 钩子 (Hook) :Tapable 提供了多种类型的钩子,如
SyncHook(同步钩子)、AsyncSeriesHook(异步串行钩子)等,每种钩子都有不同的事件执行机制。 - 事件注册 (tap) :插件通过
tap方法向钩子注册监听函数,这些函数将在钩子被触发时按顺序执行。 - 事件触发 (call) :通过
call方法触发钩子,从而按顺序执行所有已注册的监听函数。
工作原理
- 实例化钩子:在 webpack 的某个对象上,会实例化一个 Tapable 的钩子,例如
new SyncHook(['name'])。 - 事件注册:插件使用
tap方法注册一个函数到这个钩子上,例如hook.tap('myPlugin', (name) => console.log(name))。 - 事件触发:在 webpack 的构建过程中,当该钩子的时机到达时,会调用
hook.call('value')方法。 - 函数执行:
call方法会按顺序执行所有已注册的函数,并将传入的参数传递给这些函数
按执行模式分类
- Basic Hook: 基本类型的钩子,它仅仅执行钩子注册的事件,并不关心每个被调用的事件函数返回值如何。
graph LR
Start --> 事件1 --> 事件2 --> 事件3 -->Stop
- Waterfall: 瀑布类型的钩子,瀑布类型的钩子和基本类型的钩子基本类似,如果前一个事件函数的结果 result !== undefined,则 result 会作为后一个事件函数的第一个参数
graph LR
Start --> 执行函数fn1返回result1 --传递参数result1--> 执行函数fn2参数为result1返回undefined --传递参数result1--> 执行函数fn3参数result1 -->Stop
- Bail:保险类型钩子,保险类型钩子在基础类型钩子上增加了一种保险机制,执行每一个事件函数,遇到第一个结果 result !== undefined 则返回,整个钩子执行过程会立即中断,之后注册事件函数就不会被调用了。
graph LR
Start --> 事件1 --> conditionA{result!==undefined}
conditionA--Y-->Stop
conditionA--N--> 事件2 --> Stop
- Loop:循环类型钩子通过 call 调用时,如果任意一个注册的事件函数返回值非 undefeind ,那么会立即重头开始重新执行所有的注册事件函数,直到所有被注册的事件函数结果 resule === undefined。
graph LR
Start --> 事件1--> conditionA{result1!==undefined}
conditionA--Y-->Start
conditionA--N--> 事件2
事件2 --> conditionB{result2!==undefined}
conditionB--Y-->Start
conditionB--N--> 事件3
事件3 -->Stop
-
Parallel并,有点类似于
Promise.all,就是当一个hook注册了多个回调方法,这些回调同时开始并行执行。 -
Series串行,就是当一个
hook注册了多个回调方法,前一个执行完了才会执行下一个。
Parallel和Series的概念只存在于异步的hook中,因为同步hook全部是串行的。
10个钩子
同步:
- SyncHook, # 同步钩子
- SyncBailHook, # 同步熔断钩子
- SyncWaterfallHook, # 同步流水钩子
- SyncLoopHook, # 同步循环钩子
异步并发:
- AsyncParallelHook, # 异步并发钩子
- AsyncParallelBailHook, # 异步并发熔断钩子
异步串行
- AsyncSeriesHook, # 异步串行钩子
- AsyncSeriesBailHook, # 异步串行熔断钩子
- AsyncSeriesWaterfallHook # 异步串行流水钩子
- AsyncSeriesLoopHook # 异步串行循环钩子
tapable使用
使用简单来说就是下面步骤
- 实例化构造函数 Hook new SyncHooK
- 注册tap(一次或者多次)
- 执行call(传入参数)
- 如果有需要还可以增加对整个流程(包括注册和执行)的监听-拦截器
SyncHooks示例
tap接收两个参数: 第一个仅仅是一个注释的作用,第二个参数就是一个回调函数,
const { SyncHook } = require("tapable");
// 实例化一个hook
const accelerate = new SyncHook(["age"]);
// 注册第一个回调
accelerate.tap("fun1", (age) =>
console.log("fun1", `age${age}`)
);
// 再注册一个回调
accelerate.tap("fun2", (age) => {
if (age > 18) {
console.log("fun2", "已经成年!");
}
});
// 再注册一个回调
accelerate.tap("fun3", (age) => {
if (age > 60) {
console.log("fun3", "退休吧。。。");
}
});
// 触发事件
accelerate.call(66);
//------输出------
fun1 age66
fun2 已经成年!
fun3 退休吧。。。
SyncBailHook
const { SyncBailHook } = require("tapable");
// 实例化一个hook
const accelerate = new SyncBailHook(["age"]);
// 注册第一个回调
accelerate.tap("fun1", (age) =>
console.log("fun1", `age${age}`)
);
// 再注册一个回调
accelerate.tap("fun2", (age) => {
if (age > 18) {
console.log("fun2", "已经成年!");
return new Error('已经成年!');
}
});
// 再注册一个回调
accelerate.tap("fun3", (age) => {
if (age > 60) {
console.log("fun3", "退休吧。。。");
}
});
// 触发事件
accelerate.call(66);
//------输出------
fun1 age66
fun2 已经成年!
SyncWaterfallHook
const { SyncWaterfallHook } = require("tapable");
// 实例化一个hook
const accelerate = new SyncWaterfallHook(["age"]);
// 注册第一个回调
accelerate.tap("fun1", (age) => {
console.log("fun1", `age${age}`);
return "fun1";
});
// 再注册一个回调
accelerate.tap("fun2", (data) => {
console.log("fun2参数:", data)
});
// 再注册一个回调
accelerate.tap("fun3", (data) => {
console.log("fun3参数", data)
return 'fun3';
});
// 触发事件
const lastPlugin = accelerate.call(66);
console.log(`最后一个插件是:${lastPlugin}`);
//------输出------
fun1 age66
fun2参数: fun1
fun3参数: fun1
最后一个插件是: fun3
SyncLoopHook
const { SyncLoopHook } = require("tapable");
const accelerate = new SyncLoopHook(["person"]);
// Use an object so taps can mutate age across loop iterations
const person = { age: 0 };
accelerate.tap("ageChecker", (p) => {
// When age <= 5, print it
console.log(`age: ${p.age}`);
p.age++;
if (p.age <= 5) {
return true;
}
});
accelerate.call(person);
// 输出
age: 0
age: 1
age: 2
age: 3
age: 4
age: 5
AsyncParallelHook示例
异步: tapAsync和callAsync
const { AsyncParallelHook } = require("tapable");
const accelerate = new AsyncParallelHook(["age"]);
console.time("total time"); // 记录起始时间
// 注意注册异步事件需要使用tapAsync
// 接收的最后一个参数是done,调用他来表示当前任务执行完毕
accelerate.tapAsync("fun1", (age, done) => {
setTimeout(() => {
console.log("fun1", `age${age}`);
done();
}, 1000);
});
accelerate.tapAsync("fun2", (age, done) => {
setTimeout(() => {
if (age > 18) {
console.log("fun2", "成年");
}
done();
}, 2000);
});
accelerate.tapAsync("fun3", (age, done) => {
// 2秒后检测是否损坏
setTimeout(() => {
if (age > 60) {
console.log("fun3", "退休吧。。。");
}
done();
}, 2000);
});
accelerate.callAsync(66, () => {
console.log("任务全部完成");
console.timeEnd("total time"); // 记录总共耗时
});
//------输出------
fun1 age66
fun2 成年
fun3 退休吧。。。
任务全部完成
total time: 2.005s
AsyncParallelHook示例 异步:tapPromise和promise
const { AsyncParallelHook } = require("tapable");
const accelerate = new AsyncParallelHook(["age"]);
console.time("total time"); // 记录起始时间
// 注意注册异步事件需要使用tapPromise
// 回调函数要返回一个promise
accelerate.tapPromise("fun1", (age) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("fun1", `age${age}`);
resolve();
}, 1000);
});
});
accelerate.tapPromise("fun2", (age) => {
return new Promise((resolve) => {
setTimeout(() => {
if (age > 18) {
console.log("fun2", "成年");
}
resolve();
}, 2000);
});
});
accelerate.tapPromise("fun3", (age) => {
return new Promise((resolve) => {
setTimeout(() => {
if (age > 60) {
console.log("fun3", "退休吧。。。");
}
resolve();
}, 2000);
});
});
// 触发事件使用promise,直接用then处理最后的结果
accelerate.promise(66).then(() => {
console.log("任务全部完成");
console.timeEnd("total time"); // 记录总共耗时
});
//------输出------
fun1 age66
fun2 成年
fun3 退休吧。。。
任务全部完成
total time: 2.005s
还可以tapAsync和tapPromise混用
tapable实现
简单实现SyncHooks
class SyncHook {
constructor(args = []) {
this._args = args;
this.taps = [];
}
tap(name, fn) {
this.taps.push(fn);
}
call(...args) {
for (const fn of this.taps) {
const res = fn(...args);
// if( res !== undefined) return res; // SyncBailHook
}
}
}
module.exports = {
SyncHook
};
tapable源码考虑了哪些问题
- 由于在
webpack打包构建的过程中,会有上千(数量其实是取决于自身业务复杂度)个插件钩子执行,同时同类型的钩子在执行时,函数参数固定,函数体相同,因此tapable针对这些业务场景进行了相应的优化。 tapable采用new Function动态生成函数执行体的方式,同时兼顾效率(缓存)以及灵活性(容易扩展,能支持传入数量不固定的一组方法)tapable最主要的源码在Hook.js以及HookCodeFactory.js中。Hook.js主要是提供了tap、tapAsync、tapPromise等方法,每个Hook都在构造函数内部调用const hook = new Hook()初始化hook实例。HookCodeFactory.js主要是根据new Function动态生成函数执行体。- 为什么需要
CALL_DELEGATE?CALL_DELEGATE动态生成函数执行体。并且this.call被设置成this._createCall的返回值缓存起来,如果this.taps改变了,则需要重新生成。 此时如果我们第二次调用testhook.call时,就不需要再重新动态生成一遍函数执行体 - 为什么每次调用 testhook.tap() 注册插件时,都需要重置this.call等方法? 如果我们调用了n次
testhook.call,然后又调用testhook.tap注册插件,此时this.call已经不能重用了,需要再根据CALL_DELEGATE重新生成一次函数执行体, - 为什么需要在构造函数中绑定
this.compile、this.tap、this.tapAsync以及this.tapPromise等方法, 实际上这和 V8 引擎的Hidden Class有关,通过在构造函数中绑定这些方法,类中的属性形态固定,这样在查找这些方法时就能利用 V8 引擎中Hidden Class属性查找机制,提高性能。 - 源码中还加入了拦截器