2020.2.28
前言
看webpack源码就绕不过Tapable,Tapable介绍里都会提到这么一句:Tapable是个类似于Node.js EventEmitter的库。于是不禁发问:EventEmitter有什么场景无法满足事件管理,导致开发了个Tapable呢?
本文带着这个问题,对Tapable是什么、如何使用、底层如何实现等知识进行介绍。
NodeJS EventEmitter
先来看一个使用EventEmitter注册自定义事件并触发的例子:
const EventEmitter = require('events').EventEmitter;
const events = new EventEmitter()
// 注册自定义事件
events.on('login', (param) => {
console.log('triggered', param)
})
events.once('logout', function() {
console.log('logout triggered')
})
// 触发
events.emit('login', 'success') // triggered sucess
events.emit('loginout') // logout triggered
EventEmitter:
1.提供了on emit once三个方法
2.如果给一个事件订阅了多个监听器,那么会按注册顺序执行
那么,如果一个事件名称注册了多个监听器,如何控制这些监听器的执行顺序?如何在监听器之间传递值?如何中止某个监听器的执行?监听器有多个异步操作,想等这些异步操作都执行完了如何处理?
似乎做不到,猜想也许正是由于这些问题,产生了Tapable.
Tapable是什么
一个用于自定义事件的触发和处理管理的库,比EventEmitter强大。它定义了多种事件类型,能以更多方式控制监听器的执行,具体来说,
Tapable提供了9个钩子类型,使用不同类型的钩子自定义事件,能覆盖以下场景:
- 连续地执行监听器
- 并行地执行监听器
- 一个接一个地执行监听器,从前面的监听器获取输入
- 异步地执行监听器
- 在允许时停止执行监听器
Tapable是webpack的一个核心组件,但它也可以用于其他类似提供插件接口场景的应用。
webpack中很多对象是继承自Tapable的.
Tapable的9个钩子类型
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
| 序号 | 钩子名称 | 执行方式 | 特点 |
|---|---|---|---|
| 1 | SyncHook | 同步串行 | 不关心监听函数返回值 |
| 2 | SyncBailHook | 同步串行 | 监听函数中只要有一个return 有值,后面的就不执行 |
| 3 | SyncWaterfallHook | 同步串行 | 上一个监听函数返回值可以传递给下一个监听函数 |
| 4 | SyncLoopHook | 同步循环 | 监听函数返回true则一直循环执行,返回undefined则停止 |
| 5 | AsyncParallelHook | 异步并行 | 不关心监听函数返回值 |
| 6 | AsyncParallelBailHook | 异步并行 | 监听函数返回值不为null则后面的不执行,然后执行callAsync的回调函数(如果有的话) |
| 7 | AsyncSeriesHook | 异步串行 | 不关心监听函数的参数 |
| 8 | AsyncSeriesWaterfallHook | 异步串行 | 监听函数的参数不为null,则直接执行callAsync的回调函数(如果有的话) |
| 9 | AsyncSeriesBailHook | 异步串行 | 上一个监听函数callback(err, data)的第二个参数data作为值传递给下一个监听函数的参数 |
分类
每个钩子都可以订阅一个或者多个function。根据这些function如何执行可以将钩子分为4类:
- basic hook 按顺序执行
- waterfall 瀑布流式执行,与basic hook不同的是,可以在相邻的function之间传值
- bail 允许你提前退出,如果有一个function有返回值,则停下来,后面的function就不执行了
- loop 允许循环执行function
换个维度,根据钩子是同步的还是异步的,还可以分为3类:
- Sync 同步函数
- AsyncSeries,异步function串行执行
- AsyncParallel,异步funciton并行执行
分类
每个钩子都可以订阅一个或者多个function。根据这些function如何执行可以将钩子分为4类:
- basic hook 按顺序执行
- waterfall 瀑布流式执行,与basic hook不同的是,可以在相邻的function之间传值
- bail 允许你提前退出,如果有一个function有返回值,则停下来,后面的function就不执行了
- loop 允许循环执行function
换个维度,根据钩子是同步的还是异步的,还可以分为3类:
- Sync 同步函数
- AsyncSeries,异步function串行执行
- AsyncParallel,异步funciton并行执行
Tapable的使用
基本用法
注册
注册:tap/tapAsync/tapPromise
其中同步钩子使用tap注册,异步钩子使用tapAsync/tapPromise注册(效果不同)
调用
call/callAsync
Tapable SyncHook Demo:
const { SyncHook } = require('tapable')
const hook = new SyncHook(['arg1', 'arg2']) // 自定义callback的参数
hook.tap('eventname', (arg1, arg2, arg3) => {
console.log(arg1, arg2, arg3) // 1, undefined, undefined
})
hook.call(1)
const { SyncHook } = require('tapable')
class Car {
constructor() {
this.hooks = {
accelarate: new SyncHook(['newSpeed']),
brake: new SyncHook(),
}
}
}
const myCar = new Car()
myCar.hooks.accelarate.tap('eventname1', (speed) => {
console.log('speed cb 1:', speed)
})
myCar.hooks.accelarate.tap('eventname2', (speed) => {
console.log('speed cb 2:', speed)
})
myCar.hooks.accelarate.call(50)
拦截器interception
tapable还提供了拦截器。所有的钩子都提供了拦截API, 共有register, call, tap, loop 四个API,这里不展开讲
原理实现
从上面的demo来看,Tapable的使用方式与EventEmitter还是不太一样的。(不同于on emit)那么它是如何实现事件的监听与触发的呢(call方法执行时如何找到监听器函数并按规则执行)?
SyncHook源码阅读
我们以SyncHook为例,了解下内部机制:
Tapable声明了四个类
class Hook {} // 基础的Hook类
class SyncHook extends Hook {} // 同步钩子类
class HookCodeFactory {} // 用于生成hook代码的工厂类,类里的create方法使用new Function 为sync, async, promise三类钩子生成call时的fn
class SyncHookCodeFactory extends HookCodeFactory {}
其中,HookCodeFactory是编译生成可执行 fn 的工厂类,这意味着Hook的函数体代码是拼接生成的, HookCodeFactory类里的create方法使用new Function 为sync, async, promise三类钩子生成函数代码
初始化
class Hook {
// ...
}
Object.defineProperties(Hook.prototype, {
_call: {
value: createCompileDelegate("call", "sync"),
configurable: true,
writable: true
},
} // constructor里: this.call = this._call;
其中createCompileDelegate:
function createCompileDelegate(name, type) {
return function lazyCompileHook(...args) {
this[name] = this._createCall(type); // name: 'call'
return this[name](...args);
};
}
这里lazyCompileHook是个惰性函数 ,创建this.call函数,并在下一次使用时直接返回(提升性能)
其中_createCall是Hook类的成员函数:
class Hook {
// ...
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
// ...
}
其中compile:
class Hook {
// ...
compile(options) {
factory.setup(this, options); // 取出taps里的fn
return factory.create(options); // factory即是上述四个类中的SyncHookCodeFactory,create用来生成代码
}
// ...
}
factory.create根据"call", "sync"两个参数,生成了方法的函数体代码,这样就定义好了call函数:
class HookCodeFactory {
create(options) {
this.init(options);
let fn;
switch (this.options.type) {
case "sync":
fn = new Function( // 生成动态的function
this.args(),
'"use strict";\n' +
this.header() +
this.content({ // this.content 每种类型的钩子不同的实现 会去取this.taps
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
resultReturns: true,
onDone: () => "",
rethrowIfPossible: true
})
);
break;
case "async":
fn = new Function(
this.args({
after: "_callback"
}),
'"use strict";\n' +
this.header() +
this.content({
onError: err => `_callback(${err});\n`,
onResult: result => `_callback(null, ${result});\n`,
onDone: () => "_callback();\n"
})
);
break;
case "promise":
let errorHelperUsed = false;
const content = this.content({
onError: err => {
errorHelperUsed = true;
return `_error(${err});\n`;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => "_resolve();\n"
});
let code = "";
code += '"use strict";\n';
code += "return new Promise((_resolve, _reject) => {\n";
if (errorHelperUsed) {
code += "var _sync = true;\n";
code += "function _error(_err) {\n";
code += "if(_sync)\n";
code += "_resolve(Promise.resolve().then(() => { throw _err; }));\n";
code += "else\n";
code += "_reject(_err);\n";
code += "};\n";
}
code += this.header();
code += content;
if (errorHelperUsed) {
code += "_sync = false;\n";
}
code += "});\n";
fn = new Function(this.args(), code);
break;
}
this.deinit();
return fn;
}
}
这里的new Function解释下:我们知道,js中定义函数常用的有:
// 定义1. 函数声明
function add(a, b){
return a + b
}
// 定义2. 函数表达式
const add = function(a, b){
return a + b
}
```js
还有第三种,我们平常用的相对较少:
```js
// 定义3. new Function
const add = new Function('a', 'b', 'return a + b')
他们的区别就是前两种是静态的,函数的功能是做什么是在定义时就定下来了;而第三种是动态的,也就是函数的功能可能会随程序运行发生变化。
tap收集监听器
class Hook {
constructor(args) {
this.taps = [];
this.call = this._call;
}
tap(options, fn) {
this._insert(options);
}
_insert(item) {
// ...
// 检查name
if (before.has(x.name)) {
before.delete(x.name);
continue;
}
this.taps[i] = item; // this.taps里保存了所有的监听器
// ...
}
}
示例
假设有下列demo使用代码:
const { SyncHook } = require('tapable')
class Car {
constructor() {
this.hooks = {
accelarate: new SyncHook(['newSpeed']),
brake: new SyncHook(),
}
}
}
const myCar = new Car()
myCar.hooks.accelarate.tap('eventname1', (speed) => {
console.log('speed cb 1:', speed)
})
myCar.hooks.accelarate.tap('eventname2', (speed) => {
console.log('speed cb 2:', speed)
})
myCar.hooks.accelarate.call(50) // createCall时,已经根据之前tap,生成了call时的代码。 call执行时,会依次执行callback代码
这里我们执行到call时,进入函数,断点看一下call方法的函数体代码:
function anonymous(newSpeed
/*``*/) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];// 注册的第一个监听器
_fn0(newSpeed); // 执行
var _fn1 = _x[1]; // 注册的第二个监听器
_fn1(newSpeed);// 执行
}
总结一下,比较让我印象深刻的是:generate code这个地方
官网上 有段介绍很棒:
The Hook will compile a method with the most efficient way of running your plugins. It generates code depending on:
- The number of registered plugins (none, one, many)
- The kind of registered plugins (sync, async, promise)
- The used call method (sync, async, promise)
- The number of arguments
- Whether interception is used
This ensures fastest possible execution.
也就是,Tapable会根据注册的监听器的数量、种类、call方法,参数个数,是否有拦截器等生成钩子的本体代码。
其他类型Hook如何管理顺序的?
SyncBailHook this.call.toString():
"function anonymous(newSpeed
/*``*/) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(newSpeed);
if(_result0 !== undefined) {
return _result0;;
} else {
var _fn1 = _x[1];
var _result1 = _fn1(newSpeed);
if(_result1 !== undefined) {
return _result1;
} else {
}
}
}"
SyncWaterfallHook this.call.toString():
"function anonymous(newSpeed
/*``*/) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(newSpeed);
if(_result0 !== undefined) {
newSpeed = _result0;
}
var _fn1 = _x[1];
var _result1 = _fn1(newSpeed);
if(_result1 !== undefined) {
newSpeed = _result1;
}
return newSpeed;
}"
AsyncParallelHook Demo:
const { AsyncParallelHook, SyncHook } = require('tapable')
class Car {
constructor() {
this.hooks = {
accelarate: new AsyncParallelHook(['newSpeed']),
brake: new SyncHook(),
}
}
}
const myCar = new Car()
// 1. tap
// myCar.hooks.accelarate.tap('eventname1', (speed) => {
// console.log('speed cb 1:', speed)
// })
// myCar.hooks.accelarate.tap('eventname2', (speed) => {
// console.log('speed cb 2:', speed)
// })
// myCar.hooks.accelarate.callAsync(50, (err) => {
// console.log('end') //
// })
// 2. tapAsync 注意这种用法里 参数多了个cb 回调
myCar.hooks.accelarate.tapAsync('eventname1', (speed, cb) => {
setTimeout(() => {
console.log(1, speed);
cb();
}, 1000);
})
myCar.hooks.accelarate.tapAsync('eventname2', (speed, cb) => {
setTimeout(() => {
console.log(2, speed);
cb();
}, 2000);
})
myCar.hooks.accelarate.callAsync(50, () => {
console.log('end') //
})
// 3.tapPromise
// myCar.hooks.accelarate.tapPromise('eventname1', (speed, cb) => {
// return new Promise(function (resolve, reject) {
// setTimeout(() => {
// console.log(1, speed);
// resolve();
// }, 1000);
// });
// })
// myCar.hooks.accelarate.tapPromise('eventname2', (speed, cb) => {
// return new Promise(function (resolve, reject) {
// setTimeout(() => {
// console.log(2, speed);
// resolve();
// }, 2000);
// });
// })
// myCar.hooks.accelarate.promise(50).then(() => {
// console.log('end')
// })
AsyncParallelHook this.callAsync.toString():
function anonymous(
newSpeed,
_callback,
/*``*/
) {
'use strict';
var _context;
var _x = this._x;
do {
var _counter = 2;
var _done = () => {
_callback();
};
if (_counter <= 0) break;
var _fn0 = _x[0];
_fn0(newSpeed, _err0 => {
if (_err0) {
if (_counter > 0) {
_callback(_err0);
_counter = 0;
}
} else {
if (--_counter === 0) _done();
}
});
if (_counter <= 0) break;
var _fn1 = _x[1];
_fn1(newSpeed, _err1 => {
if (_err1) {
if (_counter > 0) {
_callback(_err1);
_counter = 0;
}
} else {
if (--_counter === 0) _done();
}
});
} while (false);
}
AsyncParallel counter计数器,每执行完一个计数器减一,减到0则执行回调
function anonymous(
newSpeed,
_callback
/*``*/
) {
"use strict";
var _context;
var _x = this._x;
function _next0() {
var _fn1 = _x[1];
_fn1(newSpeed, _err1 => {
if (_err1) {
_callback(_err1);
} else {
_callback();
}
});
}
var _fn0 = _x[0];
_fn0(newSpeed, _err0 => {
if (_err0) {
_callback(_err0);
} else {
_next0();
}
});
}
AsyncSeriesHook 按顺序执行,回调函数执行法
Tips
Tapable 2.0 处于beta阶段,据说针对async的执行进行了优化(减小内存消耗问题,消除递归)
webpack + Tapable
webpack 就像一条生产线,具有多个处理流程,每个流程职责单一,流程之间有依赖关系,处理完交个下个流程。
插件就像插入到生产线中的一个功能,能在特定的时机对生产线上的资源做处理。
加入生产线的方式就是监听webpack广播出来的事件。
webpack通过Tapable来组织这条生产线。
好处:有序性好、扩展性好
webpack中都使用了什么样的钩子呢?
| Compiler钩子 | 类型 | 描述 |
|---|---|---|
| done | AsyncSeriesHook | 如果有两个插件监听了done钩子,意味着两个顺序执行完了才会执行回调 |
| entryOption | AsyncBailHook | 如果有两个插件监听了entryOption钩子,意味着如果两个插件都开启了时,有一个plugin触发了如果有返回值,则另一个不会触发 |
| make | AsyncParallelHook | 如果有四个插件监听了make钩子,意味着call触发时,回调函数会等待这四个插件的监听器并行执行完才会执行 |
调试技巧
1.断点查看function body code: fn.toString()
Reference
- 编写自定义webpack插件从理解Tapable开始: juejin.im/post/5dcba2…
- webpack4.0源码分析之Tapable: juejin.im/post/5abf33… (demo较多)
- Tapable Github:github.com/webpack/tap…
- js之惰性函数:juejin.im/entry/5a629…
- 可能是全网最全最新最细的 webpack-tapable-2.0 的源码分析