tapable类似事件流机制,主要用于将webpack的plugin串联起来。这篇文章不会讲述tapable在webpack中的实际应用,而旨在说明tapable四种基础的同步Hook(
SyncHook
,SyncBailHook
,SyncLoopHook
,SyncWaterfallHook
)的使用示例和底层实现原理。
为了说明tapable的作用,我们先简单来看下使用示例吧:
const { SyncHook} = require('tapable');const hook = new SyncHook(['arg1', 'arg2', 'arg3']);/** * 注册拦截器 */hook.intercept({ register: options => { const originTapFun = options.fn; options.fn = (...params) => { console.error('before interceptor'); originTapFun(...params); } return options; }});/** * 注册监听事件 */hook.tap('sum', (arg1, arg2, arg3) => { console.log(`${arg1}+${arg2}+${arg3}:`, arg1 + arg2 + arg3);});hook.tap('multi', (arg1, arg2) => { console.log(`${arg1}*${arg2}:`, arg1 * arg2);});/** * 事件触发 */hook.call(1, 2, 3);复制代码
代码很简单,通过new SyncHook
来创建一个实例,tapable提供了intercept方法让我们可以注册拦截器.然后我们可以通过调用实例对象的tap方法来注册一个监听事件.最终使用call来触发所有注册事件. 所以最终打印结果为:
before interceptor1+2+3: 6before interceptor1*2: 2复制代码
这只是tapable提供的最简单hook,我们将会围绕这个hook来慢慢展开学习。 先简单实现一波,大致理清下整理的流程,方便理解源码:
class SyncHook { constructor(args) { this.taps = []; this.interceptors = []; } tap(options, fn) { if(typeof options === 'string') { options = { name: options}; } options = Object.assign({ fn: typeof fn === 'function' ? fn : () => {} }, options); // interceptor this._interceptorsHandle(options); this.taps.push(options); } intercept(interceptor) { this.interceptors.push(interceptor || {}); if (interceptor.register) { this.taps.map(tap => interceptor.register(tap)); } } _interceptorsHandle(options) { this.interceptors.forEach(interceptor => { if(interceptor.register) { // 可能没有返回 options = interceptor.register(options) || options; } }); return options; } call(...args) { this.taps.forEach(tap => tap.fn(...args)); }}module.exports = SyncHook;复制代码
如果只是SyncHook,那我已经学完了。但其实为了适配不同但场景,tapable提供了四种同步Hook和六种异步Hook。我们这节主要来看下四种同步Hook的使用示例和实现原理。
Sync*Hook
tapable提供了四种同步hook:SyncHook
,SyncBailHook
,SyncWaterfullHook
,SyncLoopHook
,当我们的事件处理为同步方法时我们可以使用对应的hook完成不同场景的工作.我们先看下类图:
其中Hook为一个抽象类(从java角度上触发). 我们现在主要看下Hook基类中做了哪些事情.是如何完成SyncHook工作的.
-
构造函数
具体的属性说明我已经在类图上标明清楚了.这里的args我们到后面会再做说明
// Hook.jsconstructor(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;}复制代码
- tap: 事件绑定.Hook提供了三种
tap
,tapAsync
,tapPromise
三种方法.我们先看同步的.
// Hook.jstap(options, fn) { if (typeof options === "string") options = { name: options }; if (typeof options !== "object" || options === null) throw new Error( "Invalid arguments to tap(options: Object, fn: function)" ); options = Object.assign({ type: "sync", fn: fn }, options); if (typeof options.name !== "string" || options.name === "") throw new Error("Missing name for tap"); options = this._runRegisterInterceptors(options); this._insert(options);}// 如果有注册拦截器,那么对参数进一步处理._runRegisterInterceptors(options) { for (const interceptor of this.interceptors) { // 拦截器需带有register方法. if (interceptor.register) { const newOptions = interceptor.register(options); if (newOptions !== undefined) options = newOptions; } } return options;}/** * 插入一个事件 */_insert(item) { this._resetCompilation(); // 这里我们可以通过before属性来控制tap事件的执行顺序.这也是注册 // 事件的时候为什么需要传递name属性的原因.这里会通过name来标识事件. // new Set()是需要过滤掉重复的事件. let before; if (typeof item.before === "string") before = new Set([item.before]); else if (Array.isArray(item.before)) { before = new Set(item.before); } // 另外一种方式.我们可以通过stage来控制tap事件的执行顺序,默认为0 // 事件的stage值为大,优先级越低,执行顺序越靠后 let stage = 0; if (typeof item.stage === "number") stage = item.stage; // 针对before和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;}_resetCompilation() { this.call = this._call; this.callAsync = this._callAsync; this.promise = this._promise;}复制代码
- intercept: 注册插件.这里也很简单.push到interceptors中.然后对之前对tap进行参数拦截处理.
// Hook.jsintercept(interceptor) { this._resetCompilation(); this.interceptors.push(Object.assign({}, interceptor)); if (interceptor.register) { for (let i = 0; i < this.taps.length; i++) this.taps[i] = interceptor.register(this.taps[i]); }}复制代码
- call:
事件执行
.重点是这里.
先看下call的定义.我们开始没说这里,我把代码整理一下,这样看上去更加清晰一点:
// Hook.jsthis.call = this._call;// defineObject.defineProperties(Hook.prototype, { _call: { value: createCompileDelegate("call", "sync"), configurable: true, writable: true } // ...});// createCompileDelegatefunction createCompileDelegate(name, type) { return function lazyCompileHook(...args) { this[name] = this._createCall(type); return this[name](...args); };}复制代码
这里使用了Object.defineProperties来给Hook的原型链上新增了一个_call属性.其中createCompileDelegate返回的是一个方法.当我们实例化一个SyncHook的对象的时候,此时调用call方法的时候.这时lazyCompileHook方法里的this指向的就是当前实例对象.然后初始化this.call.并执行(下次再调用call方法的时候将不需要再进行初始化,借此提供性能)
。这也就是《Javascript高级程序设计》中提到的'惰性载入函数
'。(PS:get到了吗?)
我们继续往下看.看下_createCall
的实现.
_createCall(type) { return this.compile({ taps: this.taps, interceptors: this.interceptors, args: this._args, type: type });}复制代码
非常简单.这里直接调用了compile方法进行最后的编译执行.因为每个Hook的编译执行方式都不同,所以这里将compile定义为一个抽象方法,父类Hook并没有基于此作出实现,需要子类自己去重写,否则会抛出异常(PS:后续有需要实现抽象类的可以参考这种写法
)
// Hook.jscompile(options) { throw new Error("Abstract: should be overriden");}复制代码
我们现在看下SyncHook里compile的具体实现:
// SyncHook.jsclass SyncHookCodeFactory extends HookCodeFactory { content({ onError, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onDone, rethrowIfPossible }); }}const factory = new SyncHookCodeFactory();// ...compile(options) { factory.setup(this, options); return factory.create(options);}复制代码
这里继承HookCodeFactory,这个类的作用是用于生成Hook对应的可执行代码.什么意思,我们先大致看下这个factory生成的最终代码(针对上面的例子),这样才方便进一步看懂它的内部实现:
function anonymous(arg1, arg2, arg3) { "use strict"; var _context; var _x = this._x; var _taps = this.taps; var _interceptors = this.interceptors; var _fn0 = _x[0]; _fn0(arg1, arg2, arg3); var _fn1 = _x[1]; _fn1(arg1, arg2, arg3);}复制代码
这里暂时有些迷茫的地方,this的指向我们需要继续深入了解.但对HookCodeFactory做的事情应该有了整体的把控,现在我们继续往下了解吧。
HookCodeFactory
- 构造函数
constructor(config) { this.config = config; // 生成HookCode的参数信息:{ type: "sync" | "promise" | "async", taps: Array, interceptors: Array } this.options = undefined; // 参数列表 this._args = undefined;}复制代码
- setup:执行compile的时候先执行了setup方法.这个方法主要是提取处taps里面的fn,赋值到当前对象的_x属性上.这也是生成的代码中
var x = this._x
;
setup(instance, options) { // 提取出tap的fn赋值给当前*Hook实例对象 instance._x = options.taps.map(t => t.fn);}复制代码
- create:然后调用SyncHookFactory的create方法生成可执行代码并执行.同样为了简化操作,我们这里看下大体的代码结构.代码很清晰.针对不同的类型分类处理(这里我们还是针对sync进行分析):
// HookCodeFactory.jscreate(options) { this.init(options); let fn; switch (this.options.type) { case "sync": fn = new Function( // 生成参数信息 this.args(), '"use strict";\n' + // 函数主体头部变量的声明 this.header() + // 生成函数主体部分 this.content({ // 这里参数信息如果不了解意思,可以暂时先往下看 // 对tap事件异常处理.这里表示对于同步tap默认直接抛出异常.对于异步的这种可能不适用. // tapable会在默认参数后面追加一个_callback参数.用于异常处理 onError: err => `throw ${err};\n`, // 当前tap事件对返回结果的处理.默认直接返回当前结果值 onResult: result => `return ${result};\n`, // 当前tap事件是否需要返回执行结果 resultReturns: true, // 所有tap事件执行完成之后的操作.默认不做处理 onDone: () => "", // 是否捕获执行时异常.为false的时候捕获. rethrowIfPossible: true }) ); case "async": // dosomething... case "promise": // dosomething... } this.deinit(); return fn; } /** * @param { { type: "sync" | "promise" | "async", taps: Array, interceptors: Array }} options */ init(options) { this.options = options; this._args = options.args.slice(); } // 重置属性 deinit() { this.options = undefined; this._args = undefined; }复制代码
我们一个个看,首先是this.args().其中Function语法为:
new Function ([arg1[, arg2[, ...argN]],] functionBody);复制代码
如:new Function('a','b', 'return a+b;')
所以这里的this.args()实际上是将传递过来的参数数组转化为字符串:
// Hist// before,after用于添加前置参数和追加参数args({ before, after } = {}) { let allArgs = this._args; if (before) allArgs = [before].concat(allArgs); if (after) allArgs = allArgs.concat(after); if (allArgs.length === 0) { return ""; } else { return allArgs.join(", "); }}复制代码
参数信息已经搞定,我们再来看下函数主体信息的生成,一步一步来看,我们已经快接近尾声了:
// HookCodeFactory.jsheader() { let code = ""; // 这里暂时hold下.没想清楚这里的适用场景.感觉有点像修改执行上下文(待补充) if (this.needContext()) { code += "var _context = {};\n"; } else { code += "var _context;\n"; } // 将tap事件处理函数数组赋值给局部变量.这里的this指向当前的*Hook实例对象 // this._x在调用compile方法时通过setup赋值. code += "var _x = this._x;\n"; if (this.options.interceptors.length > 0) { code += "var _taps = this.taps;\n"; code += "var _interceptors = this.interceptors;\n"; } for (let i = 0; i < this.options.interceptors.length; i++) { const interceptor = this.options.interceptors[i]; if (interceptor.call) { code += `${ this.getInterceptor(i)}.call(${ this.args({ before: interceptor.context ? "_context" : undefined })});\n`; } } return code;}needContext() { for (const tap of this.options.taps) if (tap.context) return true; return false;}复制代码
进过这段代码的处理,我们的头部已经生成出来了:
function anonymous(arg1, arg2, arg3) { "use strict"; var _context; var _x = this._x; var _taps = this.taps; var _interceptors = this.interceptors; ...}复制代码
- content:接下来看下函数主体部分.这里同样在父类HookCodeFactory中没有实现.而是交给子类自己去重写.因为每一个Hook子类生成的主体方式都不是一样的.我们回过头看下SyncHook的实现:
// SyncHook.js// 这里只提取出以下三个值.表示将onResult和resultReturns设置为undefined.// 即表示SyncHook不关心tap事件的返回值content({ onError, onDone, rethrowIfPossible }) { return this.callTapsSeries({ // 这里针对事件的异常处理.还是一样直接抛出异常 onError: (i, err) => onError(err), // 当所有事件执行完成之后不需要做任何处理 onDone, // 不需要捕获异常. rethrowIfPossible });}复制代码
我们先看下callTapsSeries的实现,再反过来详细说明这里的参数作用(这里需要注意的是Hook分为两种类型Sync和Async.但是在执行顺序上却分为三种:Series(串行),Parallel(并行)、Loop(循环). 而这三种分别调用的是:callTapsSeries,callTapsParallel,callTapsLooping
):
// HookCodeFactory.js// 取名就非常清晰易懂.按序列化(顺序)执行事件列表callTapsSeries({ // 当前tap事件的异常处理 onError, // 当前tap事件返回结果的处理 onResult, // 是否需要返回当前tap事件的执行结果 resultReturns, // 所以tap事件执行完成的处理 onDone, // 是否需要返回事件执行完成的结果 doneReturns, // 是否需要捕获执行时异常 rethrowIfPossible}) { // 事件列表为空. 直接执行回调 if (this.options.taps.length === 0) return onDone(); // 找到非sync类型的tap的索引(上面已经讲述清楚了,这里可能是异步的, // 如AsyncSeriesHook,AsyncSeriesBailHook等同样属于并行执行的Hook) // 这里我们先只考虑同步的. 所以这里为-1; const firstAsync = this.options.taps.findIndex(t => t.type !== "sync"); // 是否需要返回执行完成之后的结果 const somethingReturns = resultReturns || doneReturns || false; let code = ""; // 这里需要多花些时间.这里采用倒序的方式生成执行代码.这里的 // current表示当前执行的结果.当循环执行完成.这个current就是最终生成的执行代码 // 所以默认当前的处理为执行完成之后的处理onDone let current = onDone; for (let j = this.options.taps.length - 1; j >= 0; j--) { const i = j; // 这里因为只考虑Sync*Hook.所以这里unroll=false const unroll = current !== onDone && this.options.taps[i].type !== "sync"; if (unroll) { code += `function _next${i}() {\n`; code += current(); code += `}\n`; current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`; } // 将上一个事件的执行完成的结果赋值给当前事件执行完成之后的处理. const done = current; // 强制中断当前处理 // skipDone:是否跳过最终的事件处理. const doneBreak = skipDone => { if (skipDone) return ""; return onDone(); }; // 执行单个事件 const content = this.callTap(i, { onError: error => onError(i, error, done, doneBreak), onResult: onResult && (result => { return onResult(i, result, done, doneBreak); }), // 当前事件的执行完成后为执行上一次tap onDone: !onResult && done, rethrowIfPossible: // 这里firstAsync < 0 || i < firstAsync表示当前taps列表中不存在 // 异步Async事件.即表示强制捕获异步事件执行时异常。 rethrowIfPossible && (firstAsync < 0 || i < firstAsync) }); // 更新当前结果 current = () => content; } code += current(); return code;}复制代码
最后我们来看下callTap的实现.即执行单独某个事件.这里就非常简单了。这里分三种类型:同步(sync),异步(async)和promise.我们同样只看sync的先:
/** * 执行单个事件 * @param {Integer} tapIndex 事件索引 * @param {Object} options */callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) { let code = ""; // 判断是否有声明tap变量 let hasTapCached = false; // 拦截器处理 for (let i = 0; i < this.options.interceptors.length; i++) { const interceptor = this.options.interceptors[i]; // 如果拦截器中也包含tap方法.则先走拦截器.即拦截器不仅仅可以修改事件参数 // 也可以通过附加tap方法. if (interceptor.tap) { // 如果没有声明tap变量.则声明 if (!hasTapCached) { // var _tap0 = _taps[0]; code += `var _tap${tapIndex} = ${ this.getTap(tapIndex)};\n`; hasTapCached = true; } // 先执行拦截器的tap方法.即: // _interceptors[0].tap([_context, ]_tap0); code += `${ this.getInterceptor(i)}.tap(${ interceptor.context ? "_context, " : "" }_tap${tapIndex});\n`; } } // 声明tap执行函数 // var _fn0 = _x[0]; code += `var _fn${tapIndex} = ${ this.getTapFn(tapIndex)};\n`; const tap = this.options.taps[tapIndex]; switch (tap.type) { case "sync": // 是否需要捕获执行时的异常. if (!rethrowIfPossible) { code += `var _hasError${tapIndex} = false;\n`; code += "try {\n"; } // 如果需要返回执行结果 if (onResult) { // var _result[0] = _fn0([_context, ]arg1, arg2....); code += `var _result${tapIndex} = _fn${tapIndex}(${ this.args({ before: tap.context ? "_context" : undefined })});\n`; } else { // 不需要的话直接执行就OK了 // _fn0([context, ]arg1, arg2....); code += `_fn${tapIndex}(${ this.args({ before: tap.context ? "_context" : undefined })});\n`; } // 如果需要捕获异常 if (!rethrowIfPossible) { code += "} catch(_err) {\n"; code += `_hasError${tapIndex} = true;\n`; code += onError("_err"); code += "}\n"; code += `if(!_hasError${tapIndex}) {\n`; } // 返回直接结果 if (onResult) { code += onResult(`_result${tapIndex}`); } if (onDone) { // 追加上一次事件的处理 code += onDone(); } if (!rethrowIfPossible) { code += "}\n"; } break; case "async": // do something... case "promise": // do something... } return code;}复制代码
这里稍微有些烧脑.以使用示例讲解下完整流程:
let current = onDone(); // 所有事件执行完成之后的处理。这里为空函数for(let j = this.options.taps.length - 1; j >= 0; j--) { 当j = 1时: done = current; callTap() => 生成的最终代码为: current = () => ' var _fn1 = _x[1]; _fn1(arg1, arg2, arg3); '; j = 0时. done = current = () => ' var _fn1 = _x[1]; _fn1(arg1, arg2, arg3); ' callTap() => 生成的最终代码为: current = () => ' var _fn0 = _x[0]; _fn0(arg1, arg2, arg3); var _fn1 = _x[1]; _fn1(arg1, arg2, arg3); '}执行完成之后.将最终执行结果的代码赋值给code.整个流程结束.复制代码
经过callTapsSeries处理之后,我们代码的主体部分也已经生成.
var _fn0 = _x[0];_fn0(arg1, arg2, arg3);var _fn1 = _x[1];_fn1(arg1, arg2, arg3);复制代码
看完整个SyncHook的实现.差不多已经了解了70%,其它30%都是针对不同场景的特殊实现.我们接下来看其它三种同步Hook的使用示例。
SyncBailHook 作用:当事件处理中有一个有返回值,则不再继续往下执行.简单看下SyncBailHookCodeFactory里的实现。这个事件会返回最终执行结果:
class SyncBailHookCodeFactory extends HookCodeFactory { content({ onError, onResult, resultReturns, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onResult: (i, result, next) => // 当当前tap的返回值不是undefined的时候.直接执行最后的回调 // 否则继续往下执行 `if(${result} !== undefined) {\n${onResult( result )};\n} else {\n${next()}}\n`, resultReturns, onDone, rethrowIfPossible }); }}复制代码
使用示例:
const { SyncBailHook} = require('tapable');const hook = new SyncBailHook(['name', 'time']);hook.tap('setTime', function(name, time) { if(time > 0 && time < 8) { console.log(`${name}:`, 'sleep'); return true; } console.log(`${name}:`, 'active');});hook.tap('study', name => console.log(`${name}:`, 'study'));hook.call('wicoder', 23);hook.call('alawn', 7);复制代码
打印最终结果为:
wicoder: activewicoder: studyalawn: sleep复制代码
我们一样看下最终生成的执行代码:
function anonymous(name, time) { "use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; var _result0 = _fn0(name, time); if(_result0 !== undefined) { return _result0; ; } else { var _fn1 = _x[1]; var _result1 = _fn1(name, time); if(_result1 !== undefined) { return _result1; ; } else { } }}复制代码
SyncWaterfallHook 作用:将上一个tap事件的返回值作为下一个tap事件的输入值.类似
reduce
。还是很简单.同样当前事件也会返回最终执行结果。我们看下SyncWaterfallHookCodeFactory
的代码:
class SyncWaterfallHookCodeFactory extends HookCodeFactory { content({ onError, onResult, resultReturns, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onResult: (i, result, next) => { // 判断如果有返回值,则赋值给第一个参数.再传递给下一个tap let code = ""; code += `if(${result} !== undefined) {\n`; code += `${ this._args[0]} = ${result};\n`; code += `}\n`; code += next(); return code; }, onDone: () => onResult(this._args[0]), doneReturns: resultReturns, rethrowIfPossible }); }}复制代码
看下简单的示例:
const { SyncWaterfallHook} = require('tapable');const hook = new SyncWaterfallHook(['a', 'b']);hook.tap('add', (a, b) => a + b);hook.tap('add2', (a, b) => a + b);console.log(hook.call(2, 3));复制代码
最终打印结果为:8,生成的执行代码为:
function anonymous(a, b) { "use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; var _result0 = _fn0(a, b); if(_result0 !== undefined) { a = _result0; } var _fn1 = _x[1]; var _result1 = _fn1(a, b); if(_result1 !== undefined) { a = _result1; } return a;}复制代码
SyncLoopHook
作用:如果某个tap事件有返回值.则会循环之前执行
的事件.这里的SyncLoopHook的执行代码执行有说过会走callTapsLooping
.这里我们不进行分析,放到下一章。
const { SyncLoopHook} = require('tapable');const hook = new SyncLoopHook(['a']);let count = 1;hook.tap('start', () => console.log('start'));hook.tap('sum', a => { if (count>=3) { console.log('end'); return; } count++; console.log('count'); return true;});hook.call(1);console.log(count);复制代码
看下执行结果:
startcountstartcountstartcountstartend3复制代码
我们预先看下生成的执行代码吧.使用do...while实现:
function anonymous(a) { "use strict"; var _context; var _x = this._x; var _loop; do { _loop = false; var _fn0 = _x[0]; var _result0 = _fn0(a); if(_result0 !== undefined) { _loop = true; } else { var _fn1 = _x[1]; var _result1 = _fn1(a); if(_result1 !== undefined) { _loop = true; } else { if(!_loop) { } } } } while(_loop);}复制代码
总结