前言
Webpack官方文档在讲述Plugin API有如下描述:
tapable 这个小型 library 是 webpack 的一个核心工具,但也可用于其他地方,以提供类似的插件接口。webpack 中许多对象扩展自 Tapable 类。这个类暴露 tap, tapAsync 和 tapPromise 方法,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。
正文
Tapable对外暴露的Hook可分为同步和异步两种类型, 这两种类型在执行时又可以分为并行和串行两种方式. 具体如下:
其中各个Hook的介绍如下:
| 序号 | 钩子名称 | 执行方式 | 使用要点 |
|---|---|---|---|
| 1 | SyncHook | 同步串行 | 不关心监听函数的返回值 |
| 2 | SyncBailHook | 同步串行 | 只要监听函数中有一个函数的返回值为非 undefined,则跳过剩下所有的逻辑 |
| 3 | SyncWaterfallHook | 同步串行 | 上一个监听函数的返回值可以传给下一个监听函数 |
| 4 | SyncLoopHook | 同步循环 | 当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环 |
| 5 | AsyncParallelHook | 异步并发 | 不关心监听函数的返回值 |
| 6 | AsyncParallelBailHook | 异步并发 | 只要监听函数的返回值不为undefined,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数 |
| 7 | AsyncSeriesHook | 异步串行 | 不关系callback()的参数 |
| 8 | AsyncSeriesBailHook | 异步串行 | callback()的参数不为undefined,就会直接执行callAsync等触发函数绑定的回调函数 |
| 9 | AsyncSeriesWaterfallHook | 异步串行 | 上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数 |
例子
class Car {
constructor() { this.hooks = { accelerate: new SyncHook(["newSpeed"]) }; } // 声明钩子
// 钩子调用
setSpeed(newSpeed) { this.hooks.accelerate.call(newSpeed); }
}
// 声明实例
const myCar = new Car();
// 注册事件1
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`[LoggerPlugin]Accelrating to ${newSpeed}`));
// 注册事件2
myCar.hooks.accelerate.tap(
{ name: "LoggerPlugin2", before: "LoggerPlugin" },
newSpeed => console.log(`[LoggerPlugin1]Accelrating to ${newSpeed}`)
);
// 注册事件3
myCar.hooks.accelerate.tap({
name: "LoggerPlugin3",
fn: newSpeed => console.log(`[LoggerPlugin3]Accelrating to ${newSpeed}`)
});
myCar.hooks.accelerate.intercept({
call: newSpeed => console.log(`The newSpeed is: ${newSpeed}`),
tap: tapInfo => console.log(`${tapInfo.name} tap was trigged!`),
register: tapInfo => {
console.log(`${tapInfo.name} is doing its jos`, JSON.stringify(tapInfo));
return tapInfo;
}
});
myCar.setSpeed(60);
执行结果如下:
LoggerPlugin2 is doing its jos {"type":"sync","name":"LoggerPlugin2","before":"LoggerPlugin"}
LoggerPlugin is doing its jos {"type":"sync","name":"LoggerPlugin"}
LoggerPlugin3 is doing its jos {"type":"sync","name":"LoggerPlugin3"}
The newSpeed is: 60
LoggerPlugin2 tap was trigged!
[LoggerPlugin1]Accelrating to 60
LoggerPlugin tap was trigged!
[LoggerPlugin]Accelrating to 60
LoggerPlugin3 tap was trigged!
[LoggerPlugin3]Accelrating to 60
具体来说, 使用Tapable Hook使用大致分为三步.
- 声明实例 var sync = new SyncHook(['a', 'b'])
- 注册若干事件 sync.tap('name', fn)
- 通过sync.call(a, b)调用第2步注册的事件(调用时call时, 才会调用_createCall生成call的执行逻辑)
生成的call方法见文章最后部分.
源码解读
核心代码分布在Hook.js和HookCodeFactory.js两个文件中. 其中前者主要实现了注册事件的逻辑, 目的是提供一个可供其他Hook继承的基类, 而后者提供生成call, callAsync和callPromise方法方法体的逻辑用于生成执行注册方法的逻辑create.
我们使用上述例子中使用的SyncHook来解读一下源码, 其他的Hook实现大同小异.
SyncHook
核心代码如下
class SyncHookCodeFactory extends HookCodeFactory {
// 在HookCodeFactory contentWithInterceptors中使用, 生成执行逻辑代码
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
const factory = new SyncHookCodeFactory();
// 生成执行逻辑的方法体
const COMPILE = function(options) {
// setup, create方法见HookCodeFactory
factory.setup(this, options);
return factory.create(options);
};
function SyncHook(args = [], name = undefined) {
// 这里使用Hook初始化一个基础钩子
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}
几个关键点:
new Hook()用于初始化一个基础Hook实例COMPILE封装了生成call执行逻辑的代码, 在调用call时会调用(见下方CALL_DELEGATE)content方法用于在执行COMPILE方法时的一些定制逻辑.
Hook.js
核心代码:
const CALL_DELEGATE = function(...args) {
this.call = this._createCall("sync");
return this.call(...args);
};
class Hook {
constructor(args = [], name = undefined) {
// 省略代码...
this.taps = [];
this.call = CALL_DELEGATE;
}
// 这里并没有实现compile逻辑, 而是在各个Hook类中对compile进行赋值, 进而在HookCodeFactory中生成执行代码
// 参考上面AsyncHook中的COMPILE
compile(options) {
throw new Error("Abstract: should be overridden");
}
_createCall(type) {
return this.compile({ //省略代码...
});
}
_tap(type, options, fn) {
// 省略代码...
options = Object.assign({ type, fn }, options);
// 传入配置参数, _runRegisterInterceptors拦截器中的register可以对options定制化修改并返回新的配置
options = this._runRegisterInterceptors(options);
this._insert(options);
}
// 注册事件
tap(options, fn) {
this._tap("sync", options, fn);
}
// 向taps事件队列中添加新的声明方法并调整注册方法的执行顺序,
_insert(item) {
this._resetCompilation();
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;
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;
}
}
关键点:
tap方法用于注册普通事件(tapAsync, tapPromise用于注册异步事件)- 核心方法
_tap会调用_insert向注册的事件队列中添加事件, 并调整事件执行顺序(如果注册事件时传入了stage,before参数的话).
题外话:
_insert中调整事件执行顺序比较绕, 可以看看笔者阅读时添加的注释. stage, before使用方法参考Hook.js测试代码
HookCodeFactory.js
核心是create方法, 具体逻辑就是通过new Function方式进行字符串拼接, 生成传参及call方法的方法体. 可以学习代码下载到本地, 自己动手执行下.
myCar.hooks.accelerate.call
newSpeed => {
var _context;
var _x = this._x;
var _taps = this.taps;
var _interceptors = this.interceptors;
_interceptors[0].call(newSpeed);
var _tap0 = _taps[0];
_interceptors[0].tap(_tap0);
var _fn0 = _x[0];
_fn0(newSpeed);
var _tap1 = _taps[1];
_interceptors[0].tap(_tap1);
var _fn1 = _x[1];
_fn1(newSpeed);
var _tap2 = _taps[2];
_interceptors[0].tap(_tap2);
var _fn2 = _x[2];
_fn2(newSpeed);
};
想说的话
核心代码虽然将注册及实现做了分离, 实现了各个Hook的解耦, 但是增加了理解难度, 另外在create方法的具体实现上感觉还有优化空间, 当前的拼接方式无疑增加了记忆成本, 大佬除外...
不妥之处, 还望指出!!