
Tapable
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
Tapable提供了上述9中hook。详细的api方法可以查看Tapable文档
Tapable主要由两个重要部分组成
- Hook
- HookCodeFactory
下面以SyncHook为例,我们看看Hook处理的整个流程。SyncHook是Tapable中最容易理解的Hook,因此作为Demo进行分析。
Demo代码如下:
class Car {
constructor() {
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
};
}
setSpeed(newSpeed) {
this.hooks.accelerate.call(newSpeed);
}
}
const myCar = new Car();
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed1 => {
console.log(`${newSpeed1}`)
});
myCar.hooks.accelerate.tap("LoggerPlugin2", newSpeed2 => {
console.log(`${newSpeed2}`)
})
myCar.hooks.accelerate.tap("LoggerPlugin3", newSpeed3 => {
console.log(`${newSpeed3}`)
})
myCar.setSpeed(100);
注册插件
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed1 => {
console.log(`${newSpeed1}`)
});
首先我们一起看看tap方法(代码经过部分删减和转换)。
tap(options, fn) {
if (typeof options === "string") options = { name: options };
options = Object.assign({ type: "sync", fn: fn }, options);
this.taps.push(item);
}
tap方法主要是把输入的两个参数(plugin的名称和plugin的主要逻辑)组成一个带有type的对象,然后存放到taps数组中。
taps数组中存放的对象如下所示。
{
name: "LoggerPlugin1",
type: "sync",
fn: (newSpeed1) => {
console.log(`${newSpeed1}`)
}
}
Hook的触发
接下来我们一起看看hook的call方法做了些什么。
this.hooks.accelerate.call(newSpeed)
其实call是一个闭包。完成了把注册好的plugin按照一定的规则执行。而这个执行的规则则是由_createCall创建。_createCall会调用compile方法,compile是由Hook的子类进行实现(这里就是由SyncHook来实现)。
class Hook {
constructor(args) {
if (!Array.isArray(args)) args = [];
this._args = args;
this.taps = [];
this.call = this._call;
this._x = undefined;
}
_createCall(type) {
return this.compile({
taps: this.taps,
args: this._args,
type: type
});
}
tap(options, fn) {...}
...
}
function createCompileDelegate(name, type) {
return function lazyCompileHook(...args) {
this[name] = this._createCall(type);
return this[name](...args);
};
}
Object.defineProperties(Hook.prototype, {
_call: {
value: createCompileDelegate("call", "sync"),
configurable: true,
writable: true
},
})
每个Hook都有一个对应的HookCodeFactory,HookCodeFactory的作用就是创建一个根据规则创建待执行plugin的函数。HookCodeFactory里面大部分代码是都是在拼接函数。
const factory = new SyncHookCodeFactory();
class SyncHook extends Hook {
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
以下我将简化SyncHookCodeFactory代码,代码和源代码并不一致,只是为了说明code是怎样生成的。
HookCodeFactory,是用动态Function构建Hook触发的Plugin执行方法。
为什么要用new Function?
因为create的过程是动态的,不可能预先写好方法,因此用动态的Function也是一种解决方案。
class SyncHookCodeFactory {
constructor() {
this.options = undefined;
this._args = [];
}
create(options) {
this.init(options);
const fn = new Function(
this.args(),
this.content()
);
return fn;
}
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
init(options) {
this.options = options;
this._args = options.args.slice();
}
content() {
let code = '"use strict";\nvar _x = this._x;\n';
if (this.options.taps.length === 0) { return code; }
for (let j = this.options.taps.length - 1; j >= 0; j--) {
code += `var _fn${j} = ${this.getTapFn(j)};\n`;
code += `_fn${j}(${this.args()});\n`;
}
return code;
}
args() {
return this._args.join(', ');
}
getTapFn(idx) {
return `_x[${idx}]`;
}
}
在本例子中(串行钩子),执行factory的create方法后,会返回一个函数,参数即为call方法传入的参数:
function anonymous(newSpeed) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(newSpeed);
var _fn1 = _x[1];
_fn1(newSpeed);
var _fn2 = _x[2];
_fn2(newSpeed);
}
需要说一下,这里的_x,其实由taps.map(t => t.fn)得到的。简单来说就是注册的plugin列表。
下面简单地把_x数组所代表的内容列出来。
// 以_x[0]为例子
_x[0] = newSpeed1 => {
console.log(`${newSpeed1}`)
}
AsyncParallelHook与AsyncSeriesHook
因为在一篇博文中看到, AsyncParallelHook和AsyncSeriesHook两个执行异步的方法(文中是settimeout),执行时间是不一致的。AsyncParallelHook和它名字一样,是并行执行的;相反AsyncSeriesHook是串行执行的。
由于名字都是带async的,给人的错觉是都是异步并行。于是做了Demo验证一下。
class Car {
constructor() {
this.hooks = {
// 这里是AsyncParallelHook与AsyncSeriesHook切换
// calculateRoutes: new AsyncParallelHook(["name"])
calculateRoutes: new AsyncSeriesHook(["name"])
};
}
useNavigationSystemAsync(name) {
this.hooks.calculateRoutes.callAsync(name, err => {
console.log(err);
});
}
}
const myCar = new Car();
myCar.hooks.calculateRoutes.tapAsync("TapAsync1", (name, cb) => {
console.log(name, 1);
cb();
});
myCar.hooks.calculateRoutes.tapAsync("TapAsync2", (name, cb) => {
console.log(name, 2);
cb();
});
myCar.useNavigationSystemAsync('webpack')
AsyncSeriesHookFactory产生的代码如下
function anonymous(name, _callback) {
"use strict";
function _next0() {
const _fn1 = _x[1];
_fn1(name, _err1 => {
if (_err1) {
_callback(_err1);
} else {
_callback();
}
});
}
const _fn0 = _x[0];
_fn0(name, _err0 => {
if (_err0) {
_callback(_err0);
} else {
_next0();
}
});
}
AsyncParallelHookFactory产生的代码如下
function anonymous(name, _callback) {
"use strict";
do {
var _counter = 2;
var _done = () => {
_callback();
};
if (_counter <= 0) { break; }
const _fn0 = _x[0];
_fn0(name, _err0 => {
if (_err0) {
if (_counter > 0) {
_callback(_err0);
_counter = 0;
}
} else if (--_counter === 0) { _done(); }
});
if (_counter <= 0) { break; }
const _fn1 = _x[1];
_fn1(name, _err1 => {
if (_err1) {
if (_counter > 0) {
_callback(_err1);
_counter = 0;
}
} else if (--_counter === 0) { _done(); }
});
if (_counter <= 0) { break; }
} while (false);
}
首先,我们可以得知如果在callback中传入参数,后续的插件都都不会执行。

- AsyncSeriesHookFactory 可以看到,执行完
_fn0(即第一个插件)后,才会调用_next0()执行_fn1。 - AsyncParallelHookFactory 则不同,所有的函数几乎是同时执行,每个回调执行完count减一,直到count为0执行done方法(done方法就是下面的这个)
err => {
console.log(err);
}
区别于EventEmitter
Tapable的写法与传统的事件驱动机制不太一样,但它做的事情都是差不多。都是需要有一个订阅“事件”方法,和触发“事件”方法。
虽然说机制比较相似,但提供了9种基本的触发策略的Tapable可以说更加强大。
相似处
先说说它们之间相似的地方,以SyncHook为例来对比的话,SyncHook基本可以用EventEmitter实现。
Tapable的tap作用相当于EventEmitter的on;而call作用就相当于emit;
// SyncHook
const accelerateHook = new SyncHook(["newSpeed"])
accelerateHook.tap("LoggerPlugin", newSpeed => {
console.log(`${newSpeed}`)
});
accelerateHook.call(100);
// Node EventEmitter
const eventEmitter = new EventEmitter();
eventEmitter.on("accelerate", newSpeed1 => {
console.log(`${newSpeed1}`)
});
eventEmitter.emit("accelerate", 100);
不同点
EventEmitter事件订阅者之间是无感知的,相互无法影响的。WebpackTapable的事件订阅者之间即可以是无感知也可以是相互影响。
举个例子说明,比如SyncWaterfallHook中前一个订阅者的回调返回值会作为后一个订阅者的输入参数。
const swfh = new SyncWaterfallHook(['param']);
swfh.tap('a', function (param) {
console.log(param);
return param + 1;
});
swfh.tap('b', function (param) {
console.log(param);
return param + 2;
});
swfh.tap('c', function (param) {
console.log(param);
});
swfh.call(1);
// console
/*
1
2
4
*/
不仅如此,Tapable还提供Interception,Context,HookMap和MultiHook等玩法。