tapable
tapable
what: webpack 事件流机制的核心库。
why : 方便 webpack 运行的各生命周期中扩展功能。
how : 核心原理依赖于发布订阅模式,类似于 nodejs 的 events 库。
基础使用
const { SyncHook } = require('tapable'); //①
const hook = new SyncHook(['name']); //②
hook.tap('register', (name) => {
//③
console.log('has register ' + name);
});
hook.tap('register2', (data) => {
//④
console.log('has register2 ' + data);
});
hook.call('event'); //⑤
// has register event
// has register2 event
- tapable 库导出的是各种钩子的集合
- 创建钩子实例的时候传参必须为数组,数组内元素为注册函数的形参
- hook 分别有注册和响应的函数
- 注册的第一个参数名为了方便代码阅读和维护,对实际运行没有影响
- 响应函数的参数,作为注册函数的实参运行
Hook 的种类
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesLoopHook,
AsyncSeriesWaterfallHook,
} = require('tapable');
Hook 最主要可分为同步和异步钩子两大类,异步钩子还可细分为串行和并行两类。
-
同步:
-
SyncHook :
是所有 hook 中最普通、功能最纯粹的,每个注册的函数都将按照注册的顺序执行。
所有执行函数参数相同。
call 函数无返回值。 -
SyncBailHook :
带 Bail 的 hook 的特点是允许注册的函数停止后续函数的执行。const hook = new SyncBailHook(['name']); hook.tap('ev1', function(name) { console.log('ev1 ' + name); return '不往下执行了'; }); hook.tap('ev2', function(name) { console.log('ev2 ' + name); }); hook.call('call'); //返回值为 '不往下执行了' //ev1 call当前函数的返回值不为
undefined则会阻止后续函数的执行。
所有执行函数参数相同。
call 函数返回值为最后一个执行函数的返回值。 -
SyncWaterfallHook :
带 Waterfall 的 hook 的特点是当前执行函数的返回值作为下一个函数执行的第一个参数。const hook = new SyncWaterfallHook(['name', 'data']); hook.tap('ev1', function(name, data) { console.log('ev1', name, data); return 'ev1 的返回值'; }); hook.tap('ev2', function(name, data) { console.log('ev2', name, data); }); hook.call('call', 'other'); // 返回值为 'ev1 的返回值' // ev1 call other // ev2 ev1 的返回值 other所有注册函数依旧按照注册顺序执行。
执行函数的参数数量相同,但第一个参数可能是上一个函数的返回值。
call 函数返回的是最后一个有返回值函数的返回值。 -
SyncLoopHook :
带 Loop 的 hook 的特点是当前执行函数返回值不为undefined时,当前函数将循环执行。const hook = new SyncLoopHook(['name']); let idx = 0; hook.tap('ev1', function(name) { console.log('ev1', name); idx++; return idx === 3 ? undefined : '再来一次'; }); hook.tap('ev2', function(name) { console.log('ev2', name); }); hook.call('call'); //ev1 call //ev1 call //ev1 call //ev2 call所有执行函数参数相同。
call 函数无返回值。
该 hook 并没有在 webpack 核心库使用,可根据业务需求做插件扩展。
-
-
异步 :
带 Async 的 hook 的均为异步钩子。
异步钩子增加了两种注册和执行函数。(异步钩子也可以使用同步注册和执行)
tapAsync注册的函数通过callAsync执行。
tapAsync注册的函数的参数最后增加一个回调函数,该回调函数的执行用来通知 hook 当前函数执行结束。
当tapAsync参数不为undefined/null时,视为出现异常,callAsync的回调函数会将次异常作为参数立即执行,往后其余注册函数的回调将不影响最终回调函数的执行。
callAsync的最后一个参数也增加一个回调函数,当所有注册函数的回调执行完成后,该回调函数最后执行。hook.tapAsync('async', function(name, cb) { setTimeout(function() { cb(); }, 10); }); hook.callAsync('jw', function() { console.log('end'); });tapPromise注册的函数通过promise执行。
tapPromise注册的函数相当于 一个 Promise 对象。
promise执行函数的效果相当于 Promise.all 的执行。
关于注册函数出现 reject 的情况根据 与相同 hook 的tapAsync出现异常情况相同。hook.tapPromise('promise', function(name) { return new Promise((res, rej) => { setTimeout(() => { console.log('promise', name); res(); }, 200); }); }); hook.promise('promiseArgs').then(() => { console.log('promise done'); });-
AsyncParallelHook :
带 Parallel 的 hook 的均为异步并行钩子,其特点是各个注册函数之间的执行互不影响。const hook = new AsyncParallelHook(['name']); hook.tapAsync('react', function(name, cb) { setTimeout(function() { console.log('react', name); cb('finish right now'); }, 500); }); hook.tapAsync('node', function(name, cb) { setTimeout(function() { console.log('node', name); cb(); }, 500); }); hook.callAsync('jw', function() { console.log('end'); }); //react jw //end //node jw此处演示
tapAsync回调执行异常的情况。
即便出现异常也不影响后置函数的执行。 -
AsyncParallelBailHook :
与 AsyncParallelHook 区别在于:
AsyncParallelHook 的callAsync的回调函数的异常参数为第一个执行时出现异常的函数。
AsyncParallelBailHook 的callAsync的回调函数的异常参数为出现异常的函数中注册顺序最靠前的函数。 -
AsyncSeriesHook :
带 Series 的 hook 的均为异步串行钩子,其特点是前置注册函数执行完成之后,后置的注册函数才开始执行。
当执行回调出现异常时,将阻断后置函数的执行,异常作为callAsync的参数执行。const hook = new AsyncSeriesHook(['name']); hook.tapAsync('ev1', function(name, cb) { setTimeout(function() { console.log('ev1', name); cb('error'); }, 1500); }); hook.tapAsync('ev2', function(name, cb) { setTimeout(function() { console.log('ev2', name); }, 1000); }); hook.callAsync('call', function(err) { console.log('finish', err); }); //ev1 call //finish error -
AsyncSeriesBailHook / AsyncSeriesWaterfallHook / AsyncSeriesLoopHook :结合以上关键字的特性可得特性。
-
总结:
| 关键字 | 含义 |
|---|---|
| Sync | 同步钩子,注册函数按注册顺序同步执行 |
| tap/call | 同步注册 /执行 |
| Async | 异步钩子,支持异步执行的注册函数 |
| tapAsync/callAsync | 异步,通过回调函数通知钩子执行结束,回调函数有传参视为抛出异常处理 |
| tapPromise/promise | 异步,按照 Promise 的规则运行 |
| Parallel | 异步并行,注册函数并行执行 |
| Series | 异步串行,注册函数串行执行 |
| Bail | 保险钩子,前置注册函数的有返回值时,后置注册函数将不执行 |
| Waterfall | 瀑布流钩子,前置注册函数的返回值将作为后置函数的参数 |
| Loop | 循环钩子,当注册函数有返回值时,将会再次执行 |
| AsyncParallelBailHook | 相对特殊,注册函数异步并行互不影响,但影响最终回调函数的执行参数 |
源码分析
PS: webpack 中的 tapable 源码不在 master 上 ,需要切到分支 tapable-1 。
exports.__esModule = true;
exports.Tapable = require('./Tapable');
exports.SyncHook = require('./SyncHook');
exports.SyncBailHook = require('./SyncBailHook');
exports.SyncWaterfallHook = require('./SyncWaterfallHook');
exports.SyncLoopHook = require('./SyncLoopHook');
exports.AsyncParallelHook = require('./AsyncParallelHook');
exports.AsyncParallelBailHook = require('./AsyncParallelBailHook');
exports.AsyncSeriesHook = require('./AsyncSeriesHook');
exports.AsyncSeriesBailHook = require('./AsyncSeriesBailHook');
exports.AsyncSeriesWaterfallHook = require('./AsyncSeriesWaterfallHook');
exports.HookMap = require('./HookMap');
exports.MultiHook = require('./MultiHook');
从入口文件可知, 引入的 tapable (index.js) 主要就是复杂导出各种钩子。
const Hook = require('./Hook');
const HookCodeFactory = require('./HookCodeFactory');
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible,
});
}
}
const factory = new SyncHookCodeFactory();
class SyncHook extends Hook {
tapAsync() {
throw new Error('tapAsync is not supported on a SyncHook');
}
tapPromise() {
throw new Error('tapPromise is not supported on a SyncHook');
}
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
module.exports = SyncHook;
const Hook = require('./Hook');
const HookCodeFactory = require('./HookCodeFactory');
class AsyncSeriesLoopHookCodeFactory extends HookCodeFactory {
content({ onError, onDone }) {
return this.callTapsLooping({
onError: (i, err, next, doneBreak) => onError(err) + doneBreak(true),
onDone,
});
}
}
const factory = new AsyncSeriesLoopHookCodeFactory();
class AsyncSeriesLoopHook extends Hook {
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
Object.defineProperties(AsyncSeriesLoopHook.prototype, {
_call: { value: undefined, configurable: true, writable: true },
});
module.exports = AsyncSeriesLoopHook;
每个钩子的结构大致如上,共同点:
- 当前钩子继承于 Hook 类。
- 通过重写
content的实现,改变当前钩子的compile方法。 - 同步钩子重写 tapAsync/tapPromise 阻止执行。
- 异步钩子在其原型上添加(重写覆盖)了
_call属性。
解析一下 Hook 类的内部:
class Hook {
constructor(args) {
if (!Array.isArray(args)) args = [];
this._args = args;
this.taps = [];
this.interceptors = [];
this.call = this._call;
this.promise = this._promise;
this.callAsync = this._callAsync;
this._x = undefined;
}
compile(options) {
throw new Error('Abstract: should be overriden');
}
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type,
});
}
tap(options, fn) {
// .... 略去非核心代码
options = Object.assign({ type: 'sync', fn: fn }, options);
this._insert(options);
}
tapAsync(options, fn) {
// ....略去非核心代码 , 与 tap 方法区别就是一个类型的传参
options = Object.assign({ type: 'async', fn: fn }, options);
this._insert(options);
}
tapPromise(options, fn) {
// ....略去非核心代码 , 与 tap 方法区别就是一个类型的传参
options = Object.assign({ type: 'async', fn: fn }, options);
this._insert(options);
}
_resetCompilation() {
this.call = this._call;
this.callAsync = this._callAsync;
this.promise = this._promise;
}
_insert(item) {
// ....略去非核心代码, 将注册事件与函数添加到 taps中
this.taps[i] = item;
}
// ....略去 hook 类中 拦截器相关方法
}
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'),
//略去非核心代码....
},
_promise: {
value: createCompileDelegate('promise', 'promise'),
//略去非核心代码....
},
_callAsync: {
value: createCompileDelegate('callAsync', 'async'),
//略去非核心代码....
},
});
module.exports = Hook;
略去拦截器实现与注册的特殊配置相关代码,以简单的例子做演示跟踪。
-
创建钩子跟踪:
const pluginHook = new SyncHook(['arg1', 'arg2']);- 原型上定义了 _call/_promise/_callAsync ,主要作用是为了防止命名冲突。
- 执行
constructor构造函数,初始化私有属性。
-
注册函数跟踪:
instanceHook.tap('evt1',func1})- 组织注册函数参数,增加到 taps 中(此处略去非核心功能)
// Hook 的 tap 中 ,此函数中包含拦截器对参数的重整 this._insert({ type: 'sync', fn: func1, name: 'evt1' }); // Hook 的 _insert 中 ,此函数中包含特殊配置改变 taps 中的注册顺序 this._resetCompilation(); this.taps[0] = { type: 'sync', fn: func1, name: 'evt1' }; -
执行注册跟踪:
instanceHook.call('param1', 'param2');- 构造函数执行时,
this.call指向了this._call,即为原型上的_call,也即为createCompileDelegate('call', 'sync')的返回值。 createCompileDelegate通过_createCall返回了一个新的call。_createCall实际是将现有的钩子内部参数及由compile生成的函数。- 上文提到各个钩子的
compile是经过钩子类中重写了实现的, 而在 Hook 类相当于占位符。
//instanceHook.call('param1', 'param2') 相当于 instanceHook.compile({ taps: [{ type: 'sync', fn: func1, name: 'evt1' }], args: ['arg1', 'arg2'], type: 'sync', })('param1', 'param2'); - 构造函数执行时,
回看 SyncHook 类中,compile的相关实现
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible,
});
}
}
const factory = new SyncHookCodeFactory();
class SyncHook extends Hook {
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
为了探寻 compile 的具体实现,需要解析一下 HookCodeFactory 内部实现
class HookCodeFactory {
constructor(config) {
this.config = config;
this.options = undefined;
this._args = undefined;
}
create(options) {
this.init(options);
let fn;
switch (this.options.type) {
case 'sync':
fn = new Function(
this.args(),
'"use strict";\n' +
this.header() +
this.content({
onError: (err) => `throw ${err};\n`,
onResult: (result) => `return ${result};\n`,
resultReturns: true,
onDone: () => '',
rethrowIfPossible: true,
})
);
break;
//省略......
}
this.deinit();
return fn;
}
setup(instance, options) {
instance._x = options.taps.map((t) => t.fn);
}
callTapsSeries({
onError,
onResult,
resultReturns,
onDone,
doneReturns,
rethrowIfPossible,
}) {
//省略......
return code;
}
callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
//省略......
return code;
}
//省略......
}
由此可看出 HookCodeFactory 是一个根据钩子的内部参数生成代码字符串,再及由 new Function() 的方式返回代码。
SyncHook 生成的 call 为例:
const SyncHook = require('./lib/SyncHook');
function func1() {
console.log('注册函数1');
}
function func2() {
console.log('注册函数2');
}
function func3() {
console.log('注册函数3');
}
const hook = new SyncHook(['arg1', 'arg2']);
hook.tap('evt1', func1);
hook.tap('evt2', func2);
hook.tap('evt3', func3);
hook.call('param1', 'param2');
function(arg1, arg2){
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(arg1, arg2);
var _fn1 = _x[1];
_fn1(arg1, arg2);
var _fn2 = _x[2];
_fn2(arg1, arg2);
}
SyncWaterfallHook 生成的 call 为例:
const SyncWaterfallHook = require('./lib/SyncWaterfallHook');
function func1() {
console.log('注册函数1');
return undefined;
}
function func2() {
console.log('注册函数2');
return 1;
}
function func3() {
console.log('注册函数3');
return undefined;
}
const hook = new SyncWaterfallHook(['arg1', 'arg2']);
hook.tap('evt1', func1);
hook.tap('evt2', func2);
hook.tap('evt3', func3);
hook.call('param1', 'param2');
function(arg1, arg2){
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(arg1, arg2);
if(_result0 !== undefined) {
arg1 = _result0;
}
var _fn1 = _x[1];
var _result1 = _fn1(arg1, arg2);
if(_result1 !== undefined) {
arg1 = _result1;
}
var _fn2 = _x[2];
var _result2 = _fn2(arg1, arg2);
if(_result2 !== undefined) {
arg1 = _result2;
}
return arg1;
}
AsyncParallelBailHook 生成的 call 为例:
const AsyncParallelBailHook = require('./lib/AsyncParallelBailHook');
function func1(name, cb) {
setTimeout(() => {
cb();
}, 10);
return undefined;
}
function func2(name, data, cb) {
console.log('注册函数2');
setTimeout(() => {
cb('this is error');
}, 10);
return 1;
}
function func3(cb) {
console.log('注册函数3');
cb();
return undefined;
}
const hook = new AsyncParallelBailHook(['arg1', 'arg2']);
hook.tapAsync('evt1', func1);
hook.tapAsync('evt2', func2);
hook.tapAsync('evt3', func3);
hook.callAsync('param1', 'param2', function(err) {
if (err) {
console.err('err');
}
console.log('finish');
});
function(arg1, arg2, _callback){
"use strict";
var _context;
var _x = this._x;
var _results = new Array(3);
var _checkDone = () => {
for (var i = 0; i < _results.length; i++) {
var item = _results[i];
if (item === undefined) return false;
if (item.result !== undefined) {
_callback(null, item.result);
return true;
}
if (item.error) {
_callback(item.error);
return true;
}
}
return false;
};
do {
var _counter = 3;
var _done = () => {
_callback();
};
if (_counter <= 0) break;
var _fn0 = _x[0];
_fn0(arg1, arg2, (_err0, _result0) => {
if (_err0) {
if (_counter > 0) {
if (
0 < _results.length &&
((_results.length = 1),
(_results[0] = { error: _err0 }),
_checkDone())
) {
_counter = 0;
} else {
if (--_counter === 0) _done();
}
}
} else {
if (_counter > 0) {
if (
0 < _results.length &&
(_result0 !== undefined && (_results.length = 1),
(_results[0] = { result: _result0 }),
_checkDone())
) {
_counter = 0;
} else {
if (--_counter === 0) _done();
}
}
}
});
if (_counter <= 0) break;
if (1 >= _results.length) {
if (--_counter === 0) _done();
} else {
var _fn1 = _x[1];
_fn1(arg1, arg2, (_err1, _result1) => {
if (_err1) {
if (_counter > 0) {
if (
1 < _results.length &&
((_results.length = 2),
(_results[1] = { error: _err1 }),
_checkDone())
) {
_counter = 0;
} else {
if (--_counter === 0) _done();
}
}
} else {
if (_counter > 0) {
if (
1 < _results.length &&
(_result1 !== undefined && (_results.length = 2),
(_results[1] = { result: _result1 }),
_checkDone())
) {
_counter = 0;
} else {
if (--_counter === 0) _done();
}
}
}
});
}
if (_counter <= 0) break;
if (2 >= _results.length) {
if (--_counter === 0) _done();
} else {
var _fn2 = _x[2];
_fn2(arg1, arg2, (_err2, _result2) => {
if (_err2) {
if (_counter > 0) {
if (
2 < _results.length &&
((_results.length = 3),
(_results[2] = { error: _err2 }),
_checkDone())
) {
_counter = 0;
} else {
if (--_counter === 0) _done();
}
}
} else {
if (_counter > 0) {
if (
2 < _results.length &&
(_result2 !== undefined && (_results.length = 3),
(_results[2] = { result: _result2 }),
_checkDone())
) {
_counter = 0;
} else {
if (--_counter === 0) _done();
}
}
}
});
}
} while (false);
}
所有钩子都可以通过打印 create 打印出相应的当前 hook 执行的 call 函数。
本文主要目的为讲解 tapable 在 webpack 中的机制, 各个钩子生成代码的部分不一一进行解析。
源码中关键字解释:
- _x: 注册函数的数组。
- _context : 当前注册函数的执行上下文。 (bail/waterfall 等钩子的返回值需要判断或传递给后置函数,或有拦截器等情况)
- interceptors: 拦截器, 高级用法。
instanceHook.intercept({ context: true, tap: (context, ...args) => { //.... }, }); instanceHook.tap( { name: 'evtType', context: true, }, (context, ...args) => { //.... } ); - before : 在参数中的特殊标识,指 _context 或 拦截器。
- after : 在参数中的特殊标识,指钩子执行结束后的最终回调函数。

浙公网安备 33010602011771号