分析tapable设计,学习事件流处理

588 阅读11分钟

这是 学习源码设计 第二篇,主要是学习源码实现原理和揣摩设计思路,不会深究每个开源库具体的函数实现。

#0 导读

本文学习的版本是2.0.0-beta.10(截止至目前20200420),拉取的tapable开源库master分支,最新一次commit75fcb00e84922de8cb0ecb9840c59b68c5ced0d3

很早以前就知道webpack本质上是一种事件流的机制,构建的流程是将各个拆件串联起来,而实现这一切的核心就是Tapable,wepack中负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。但是最近几天在看Tapable源码发现,除去这些被使用到的重要模块,version-2.0.0版本的实现惊到了我。

先看下整个tapable库的架构。

image-20200420222404369

可以看出整个tapable库围绕着Hook的概念进行展开,有同步、异步串行、异步并行钩子,每一种钩子都是一个继承自Hook的类。

Hook类系列

Hook名称 钩子使用方式 钩子作用
SyncHook tap 同步钩子
SyncBailHook tap 同步钩子,只要执行的 handler 有返回值,剩余 handler 不执行
SyncWaterfallHook tap 同步钩子,上一个 handler 的返回值作为下一个 handler 的输入值
SyncLoopHook tap 同步钩子,只要执行的 handler 有返回值,一直循环执行此 handler
AsyncParallelHook tap, tapAsync, tapPromise 异步钩子,handler 并行触发
AsyncParallelBailHook tap, tapAsync, tapPromise 异步钩子,handler 并行触发,但是跟 handler 内部调用回调函数的逻辑有关
AsyncSeriesHook tap, tapAsync, tapPromise 异步钩子,handler 串行触发
AsyncSeriesBailHook tap, tapAsync, tapPromise 异步钩子,handler 串行触发,但是跟 handler 内部调用回调函数的逻辑有关
AsyncSeriesLoopHook tap, tapAsync, tapPromise 异步钩子,可以触发 handler 循环调用
AsyncSeriesWaterfallHook tap, tapAsync, tapPromise 异步钩子,上一个 handler 可以根据内部的回调函数传值给下一个 handler

Hook工具类

名称 作用
HookCodeFactory 未对外导出,编译生成可执行 fn 的工厂类
HookMap Map 结构,存储多个 Hook 实例
MultiHook 组合多个 Hook 实例
#1 简单使用

既然tapable本质上是事件流,那么应该会有注册事件tap/tapAsync/tapPromise和触发事件call/callAsync/promise,其原理与events存在基本的相同。

const { SyncHook }= require('tapable');

// 实例化SynvHook
const taphook = new SyncHook(['speed', 'speed2'])

// 通过tap注册handler,其功能类似node的 events.on
taphook.tap('event01', (args1, args2) => {console.log("event01: ", args1, args2)})
taphook.tap({name: 'event02', before: 'event01'}, (args1, args2) => {console.log("event02: ", args1, args2)})
taphook.tap({name: 'event03', stage: -1}, (args1, args2) => {console.log("event03: ", args1, args2)})

// 通过call执行handler,其功能类似node的events.emit
taphook.call("user01-speed-params", "user02-speed2-params")

// 打印结果如下
event03:  user01-speed-params user02-speed2-params
event03:  user01-speed-params user02-speed2-params
event03:  user01-speed-params user02-speed2-params

在实例化SyncHook的时候会接受字符串数组,这个字符串数组长度与handler和call方法的参数长度相等,可理解成call传递的参数会逐个传递每个handler进行相关处理。

就像案例中通过实例化一个taphook,最后调用call传入不同学生的跑步速度,通过不同的handler钩子能够输出不同的结果。

打印结果并不是按照先注册先执行的方式,而是在tapable内部维护了一个会记录每一次tap的参数的执行handler的数组。

type Tap = TapOptions & {
	name: string; // 标记每个handler的名字
};
type TapOptions = {
	before?: string; // 插入到指定的handler之前,这里相当于有一个插入排序的算法
	stage?: number; // handler的顺序优先级,默认是0,这个值越小handler执行顺序排在越前
};

在案例中,name为event02的handler在注册的时候,传递了一个对象进去,且在参数当中指定了event02的handler需要在event01之前执行;再看到event03的handler指定了stage为-1(由于内部库在对handlers排序是staging优先级高,且before排序的默认值为0),所以,整个tap的调用handler顺序为'event03', 'event02', 'event01'

#2 探索原理

说完了这个一个案例的输出结果,接下来顺着案例SyncHook来探索下demo的实现原理

探索原理实现的细节流程如下:

  1. 问题一: 在创建SyncHook钩子对象的时候执行什么工作呢?
  2. 问题二:调用tap方法后发生了什么事情呢?
  3. 问题三:接下来看看注册拦截器做了什么事情?
  4. 问题四:拦截器如何添加进去的?
  5. 问题五:_insert函数做了什么事情呢?
  6. 问题六:案例中taphook.call发生了什么事情呢?
  7. 问题七:compile内部做了什么事情呢?
  8. 问题八:问题七当中提到的this.header()做了什么事情呢?
  9. 问题九:问题七中的this.contentWithInterceptors发生了什么事情呢?
  10. ⑨ + ①:小结:将各个问题小结一下

①问题: 在创建SyncHook钩子对象的时候执行什么工作呢?
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();

const TAP_ASYNC = () => {
	throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () => {
	throw new Error("tapPromise is not supported on a SyncHook");
};

// 编译生成对应的`fn`, 然后通过调用call方法执行生成的`fn`
const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};

// 核心,继承Hoo基类,并通过子类实现tapAsync、tapPromise、compile来自定义handler的注册方式
function SyncHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	hook.constructor = SyncHook;
	hook.tapAsync = TAP_ASYNC;
	hook.tapPromise = TAP_PROMISE;
	hook.compile = COMPILE;
	return hook;
}

SyncHook.prototype = null;

可以看出SyncHook继承自父类Hook,并且重写了tapAsync、tapPromise、compile三个方法,从SyncHook就能发现SyncHook不支持``tapAsync、tapPromise来注册异步事件流。再说compile方法是用来编译生成对应的fn, 然后通过调用call方法执行生成的fn`。

class Hook {
	constructor(args = [], name = undefined) {
    // 参数必须是数组,不然会出现异常
		this._args = args;
    // 钩子的名字,目前没有发现哪里有用到
		this.name = name;
    // 存放tap函数的参数
		this.taps = [];
    // 拦截器数组
		this.interceptors = [];
    // 暴露出去用于调用同步钩子的函数
		this._call = CALL_DELEGATE;
		this.call = CALL_DELEGATE;
    // 暴露出去的用于调用异步钩子函数
		this._callAsync = CALL_ASYNC_DELEGATE;
		this.callAsync = CALL_ASYNC_DELEGATE;
    // 暴露出去的用于调用异步promise函数
		this._promise = PROMISE_DELEGATE;
		this.promise = PROMISE_DELEGATE;
    // 用于生成调用函数的时候,保存钩子数组的变量
		this._x = undefined;

    // 继承子类的实现方法
		this.compile = this.compile;
		this.tap = this.tap;
		this.tapAsync = this.tapAsync;
		this.tapPromise = this.tapPromise;
	}

  // ... ...
}
② 问题二:调用tap方法后发生了什么事情呢?
tap(options, fn) {
	this._tap("sync", options, fn);
}

_tap(type, options, fn) {
  // 参数的限制与参数的转化
	if (typeof options === "string") {
		options = {
			name: options
		};
	} else if (typeof options !== "object" || options === null) {
		throw new Error("Invalid tap options");
	}
	if (typeof options.name !== "string" || options.name === "") {
		throw new Error("Missing name for tap");
	}
	if (typeof options.context !== "undefined") {
		deprecateContext();
	}
	options = Object.assign({ type, fn }, options);
  // 注册拦截器
	options = this._runRegisterInterceptors(options);
  // 插入钩子
	this._insert(options);
}

③ 问题三:接下来看看注册拦截器做了什么事情

_runRegisterInterceptors(options) {
  // 到现在这个参数应该是 {name: "event01", type: "sync", fn: function ... }
	for (const interceptor of this.interceptors) {
		if (interceptor.register) {
      // 把options传入拦截器注册,拦截器的register 可以返回一个新的options选项,
      // 并且替换掉原来的options选项,也就是说可以在执行了一次register之后 改变你当初 tap 进去的方法
      
      // 这种方式可以说很好理解了,就是在真正执行tap之前将options进行拦截处理,返回一个新的options
      // 在使用上需要注意一些细节,intercept需要在tap之前执行
			const newOptions = interceptor.register(options);
			if (newOptions !== undefined) {
				options = newOptions;
			}
		}
	}
	return options;
}

这种典型的拦截处理方式很好理解(常规操作),就是在真正执行tap之前将options进行拦截处理,返回一个新的options,在使用上intercept需要在tap之前执行。那么下一个问题,拦截器如何加入进去的呢?

④ 问题四:拦截器如何添加进去的?

intercept(interceptor) {
  // 重置 子类重写的方法, 官方文档提到过 编译出来的调用方法对截器存在一定的依赖性. 所有每添加一个拦截器都要重置一次调用方法,在下一次编译的时候,重新生成.
	this._resetCompilation();
  // 保存原有引用的前提下复制一份拦截器对象
	this.interceptors.push(Object.assign({}, interceptor));
	if (interceptor.register) {
    // 将taps对象作为拦截对象,写的很亲切了
		for (let i = 0; i < this.taps.length; i++) {
			this.taps[i] = interceptor.register(this.taps[i]);
		}
	}
}

⑤ 问题五:_insert函数做了什么事情呢?

_insert(item) {
  // 重置子类重写方法
	this._resetCompilation();
  // 这个before就是案例taphook.tap方法的第一个参数,用在webpack当中就是钩子的名字
	let before;
  // 这个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;
    // 默认stage为0
		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; // 把挪出来的位置插入传进来的钩子
}

一个根据before和stage来排序的简单排序算法, 可以看到stage的权重比before的权重略大

仔细想来tap其实就做了参数拦截处理调整taps的顺序两件事情,接下来看下案例中taphook.call做了什么事情

⑥ 问题六:案例中taphook.call发生了什么事情呢?

const CALL_DELEGATE = function(...args) {
  // 这个整个tapable架构被我称之为艺术的核心所在了,通过new Function()生成fn并缓存,这样做能够减少内存的消耗
	this.call = this._createCall("sync");
	return this.call(...args);
};

_createCall(type) {
	return this.compile({
		taps: this.taps,
		interceptors: this.interceptors,
		args: this._args,
		type: type
	});
}

compile(options) {
  // 强制要求子类重写
	throw new Error("Abstract: should be overridden");
}

⑦ 问题七:compile内部做了什么事情呢?

方法强制要求子类重写,来看看子类SyncHook的实现

const factory = new SyncHookCodeFactory();

const COMPILE = function(options) {
  // options:
  // {
  //  taps: this.taps, tap对象数组
  //  interceptors: this.interceptors, 拦截器数组
  //  args: this._args,
  //  type: type
  // }
 factory.setup(this, options);
 return factory.create(options);
};
// HookCodeFactory.js
class HookCodeFactory {
	constructor(config) {
    // 其他类使用到
		this.config = config;
		this.options = undefined;
		this._args = undefined;
	}
  
	setup(instance, options) {
    // instance 为SyncHook的实例,将所有的注册钩子事件函数以数组的形式 缓存 至 _x 中
		instance._x = options.taps.map(t => t.fn);
	}
  
  init(options) {
		this.options = options;
		this._args = options.args.slice();
	}
  
  // 贴个申明,详细代码看后续
  header() { }

  create(options) {
		this.init(options);
		let fn;
		switch (this.options.type) {
			case "sync":
        // 动态生成一个钩子函数
				fn = new Function(
					this.args(), // 案例当中就是 “speed, speed2”
					'"use strict";\n' +
						this.header() +
						this.contentWithInterceptors({
							onError: err => `throw ${err};\n`,
							onResult: result => `return ${result};\n`,
							resultReturns: true,
							onDone: () => "",
							rethrowIfPossible: true
						})
				);
				break;
        
        // 其他情况暂时不看 ...
        
		}
    // 把上面 init() 赋的值重置为 undefined
    // this.options = undefined;
    // this._args = undefined;
		this.deinit();
		return fn;
	}
}

⑧ 问题八:问题七当中提到的this.header()做了什么事情呢?

class HookCodeFactory { 
  
  header() {
    let code = "";
    // 遍历判断判断this.options.taps[i] 是否 有context 属性, 任意一个tap有 都会返回 true
    if (this.needContext()) {  
      // 如果有context 属性, 那_context这个变量就是一个空的对象.
      code += "var _context = {};\n";
    } else {
       // 否则 就是undefined
      code += "var _context;\n";
    }
    // 在setup()中 把所有tap对象的钩子 都给到了 instance ,这里的this 就是setup 中的instance _x 就是钩子对象数组
    code += "var _x = this._x;\n";

    if (this.options.interceptors.length > 0) {
      // 如果有拦截器, 保存taps 数组到_taps变量, 保存拦截器数组 到变量_interceptors
      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) {
        // getInterceptor 返回的 是字符串 是 `_interceptors[i]`
        // 一个拦截器只会生成一次call, 并且在call拦截器的时候传入("_context, speed, speed2")
        code += `${this.getInterceptor(i)}.call(${this.args({
          before: interceptor.context ? "_context" : undefined
        })});\n`;
      }
    }
    // 返回一个可以执行的javascript字符串
    // 我们的例子中返回的是 "var _context;\nvar _x = this._x;\n"
    return code;
  }


  getInterceptor(idx) {
    return `_interceptors[${idx}]`;
  }
}

⑨ 问题九:问题七中的this.contentWithInterceptors发生了什么事情呢?

class HookCodeFactory { 
  contentWithInterceptors(options) {
    if (this.options.interceptors.length > 0) {
      const onError = options.onError;
      const onResult = options.onResult;
      const onDone = options.onDone;    
      // 对参数做出相应处理
      return this.content(
        // ... ...
      );
    } else {
      return this.content(options);
    }
  }
  // ...
}

class SyncHookCodeFactory extends HookCodeFactory {
  // 实际上调用了callTapsSeries 事件,见上面的代码
	content({ onError, onDone, rethrowIfPossible }) {
    // 改变了onError事件,暂时没有想明白为什么这样做
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}

class HookCodeFactory { 
  callTapsSeries({
    onError, // 案例中传参了
    onResult, // 案例中为undefined
    resultReturns, // 案例中为undefined
    onDone, // 案例中传参了
    doneReturns, // 案例中为undefined
    rethrowIfPossible // 案例中传参了
  }) {
    // onDone返回一个空串“”
    if (this.options.taps.length === 0) return onDone();
    // 如果存在异步钩子,把=获取第一个异步钩子的下标,如果没有这个返回的是-1
    const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
    // 我们的案例这里为false
    const somethingReturns = resultReturns || doneReturns || false;
    let code = "";
    let current = onDone;
    for (let j = this.options.taps.length - 1; j >= 0; j--) {
      const i = j;
      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;
    	// 字面意思,是否跳过done 是就增加一个跳出递归的条件
      const doneBreak = skipDone => {
        if (skipDone) return "";
        return onDone();
      };
      // callTap的调用代码具体看下面的函数
      const content = this.callTap(i, {
        // 调用的onError 是 (i, err) => onError(err) , 后面这个onError(err)是 () => `throw ${err}`, 会将i丢弃
      	// 目前 i done doneBreak 三个参数都没有使用到
        onError: error => onError(i, error, done, doneBreak),
        // 这里onResult 同步钩子的情况下在外部是没有传进来的,刚才也提到了
     		// 这里onResult是 undefined
        onResult:
          onResult &&
          (result => {
            return onResult(i, result, done, doneBreak);
          }),
        // 没有onResult 一定要有一个onDone 所以这里就是一个默认的完成回调
      	// 这里的done 执行的是next(i+1), 也就是迭代的处理完所有的taps
        onDone: !onResult && done,
        // rethrowIfPossible 默认是 true 也就是返回后面的
        // 因为没有异步函数 firstAsync = -1.
        // 所以返回的是 -1 < 0,也就是true, 这个可以判断当前的是否是异步的tap对象
        // 这里处理的妙啊 如果是 false 那么当前的钩子类型就不是sync,可能是promise或者是async
        // 具体作用要看callTaps()如何使用这个.
        rethrowIfPossible:
          rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
      });
      // 这里没看懂,惰性执行吗?
      current = () => content;
    }
    code += current();
    return code;
  }
  
  /** 
  * tapIndex 当前钩子所在的游标位置
  * onError:() => onError(i,err,done,skipdone) ,
  * onReslt: undefined
  * onDone: () => {return: done()} //递归结束
  * rethrowIfPossible: false 说明当前的钩子不是sync的.
  */
  callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
    let code = "";
    // hasTapCached 是否有tap的缓存
    let hasTapCached = false;
    for (let i = 0; i < this.options.interceptors.length; i++) {
      const interceptor = this.options.interceptors[i];
      if (interceptor.tap) {
        if (!hasTapCached) {
          // 这里生成的代码就是 `var _tap0 = _taps[0]`
        	// _taps 变量我们在 header 中生成
          code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
          // 如果这里执行多次,就会生成多个重复代码,不稳定,也影响性能.
          hasTapCached = true;
        }
        // 我们的案例产生的其中一个code结果
        // _interceptor[0].tap(_tap0);
        code += `${this.getInterceptor(i)}.tap(${
          interceptor.context ? "_context, " : ""
        }_tap${tapIndex});\n`;
      }
    }
    // 这里在案例中的code为 var _fn0 = _x[0]
    code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
    
    // 开始处理tap对象
    const tap = this.options.taps[tapIndex];
    switch (tap.type) {
      case "sync":
        // 在可能抛出错误的情况下,增加try {} catch(e) {}
        if (!rethrowIfPossible) {
          code += `var _hasError${tapIndex} = false;\n`;
          code += "try {\n";
        }
        
        // 同步的时候 onResult 是 undefined
        // 我们也分析一下如果走这里会怎样
        // var _result0 = _fn0(option)
        // 可以看到是调用tap 进来的钩子并且接收参数
        // ps:看官网介绍时好几个返回undefined的钩子,看着就在猜这个问题的结果,源码看到这里我笑了,果然如果猜测一样,有就接受结果,哈哈~
        if (onResult) {
          code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
            before: tap.context ? "_context" : undefined
          })});\n`;
        } else {
          code += `_fn${tapIndex}(${this.args({
            before: tap.context ? "_context" : undefined
          })});\n`;
        }
        
        // 把 catch 补上,在这个例子中没有
        if (!rethrowIfPossible) {
          code += "} catch(_err) {\n";
          code += `_hasError${tapIndex} = true;\n`;
          code += onError("_err");
          code += "}\n";
          code += `if(!_hasError${tapIndex}) {\n`;
        }
        // 有onResult 就把结果给传递出去.
        if (onResult) {
          code += onResult(`_result${tapIndex}`);
        }
        // 有onDone() 就调用他开始递归,上面callTapsSeries函数的next(i)
        if (onDone) {
          code += onDone();
        }
        if (!rethrowIfPossible) {
          code += "}\n";
        }
        break;
        // case "async" 和 case "promise"
        // ... ...
    }
    // 我们的例子中,code为 "var _fn2 = _x[2];\n_fn2(speed, speed2);\n"
    return code;
  }
  
  // ...
}

⑨ + ①:小结:将各个问题小结一下

来看看this._createCall具体执行结果是什么

// {
// 	taps: this.taps,
// 	interceptors: this.interceptors,
// 	args: this._args,
// 	type: type
// }
const CALL_DELEGATE = function(...args) {
  // 现在回味这句话,你觉得能称之为艺术吗?
  // 这个整个tapable架构被我称之为艺术的核心所在了,通过new Function()生成fn并缓存,这样做能够减少内存的消耗
  
  // this.call 就是上面的匿名函数
	this.call = this._createCall("sync");
	return this.call(...args);
};

经过上述分析得出,我们案例中相关的参数在this.call执行时,具体如下

// ① _args
[ 'user01-speed-params', 'user02-speed2-params' ]

// ② this._x
[
  (args1, args2) => {console.log("event03: ", args1, args2)}
  (args1, args2) => {console.log("event02: ", args1, args2)}
  (args1, args2) => {console.log("event01: ", args1, args2)}
]

// ③ this.call执行函数,this._createCall("sync")的返回值
function anonymous(speed, speed2
) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(speed, speed2);
var _fn1 = _x[1];
_fn1(speed, speed2);
var _fn2 = _x[2];
_fn2(speed, speed2);

}

最后看看匿名函数执行结果是多少

event03:  user01-speed-params user02-speed2-params
event02:  user01-speed-params user02-speed2-params
event01:  user01-speed-params user02-speed2-params

经过九个问题加上一个小结,我们追踪跟进发现taphook实例、tap和call实际执行的函数和相关结果了,到这里也就差不多将整个taphook的核心基本了解明白了。

#3 小结一下

小结一下整个demo的执行流程

  • ① 实例创建:创建SyncHook实例,通过继承的方式重写相关方法
  • ② Tap方法:先校验 options 参数的格式,再走到 _runRegisterInterceptors 方法,这一步是为了执行拦截器的 register 方法,来生成新的 options。
  • ② Tap方法:接着走到 _insert 内部,内部根据 before、stage 属性来调整 handler 的顺序(我的理解是stage的权重比before略大),并且将所有的信息保存到 taps 数组里面。
  • ③ Call方法:执行 call,就是执行了代理函数CALL_DELEGATE,也就是执行了 _createCall, 通过call来执行_createCall返回惰性函数字符串
  • ④ Call方法:_createCall 内部执行了 compile 方法,这个方法在 SyncHook 的原型上。compile 的内部先执行 SyncHookCodeFactory 上的 setup 方法,然后执行 create 方法。setup 与 create 方法都是在 HookCodeFactory 的原型上,因为 SyncHookCodeFactory 是继承于 HookCodeFactory。
  • ⑤ Call方法:setup 内部的逻辑很简单,就是从 taps 数组的值传递给_x,供后续create生成的函数调用
  • ⑥ Call方法:create 内部先初始化 options 参数,这个是在调用 compile 的时候传入的,然后通过字符串拼接执行 new Function 得到 fn,最后执行的也是这个 fn。
#4 调试部分

如果你还想看下其他的异步钩子如何执行,只需要写好demo通过对应的工具调试即可。贴出我在学习源码是写的一个异步hook的demo,方便调试使用

const { AsyncParallelHook,AsyncSeriesWaterfallHook,AsyncSeriesHook }= require('tapable');

class Person{
    constructor(){
    		//所有的钩子都暴露在hooks属性上--官方推荐用法
        this.hooks={
            //速度
            run:new AsyncParallelHook(['speed']),
            runSeries:new AsyncSeriesHook(['speed']),
            runPromise:new AsyncSeriesWaterfallHook(['speed'])
        }
    }
    runAsync(speed){
        this.hooks.run.callAsync(speed,()=>{
            console.info('runAsync:Async Parallel call complete!!!');
        });
    }
    runSeries(speed){
        this.hooks.runSeries.callAsync(speed,()=>{
            console.info('runSeries:Async Series call complete!!!');
        });
    }
    runPromise(speed){
        this.hooks.runPromise.promise(speed).then(ret=>{
            console.info(`runPromise:Async Waterfall Promise call ret===${ret}`)
        })
    }
}

//记录跑的速度 Phone  SportsBracelet ...
const student =new Person();


/**
 * Parallel
 */
student.hooks.run.tapAsync('phone',(speed,done)=>{
    setTimeout(()=>{
        console.info(`Async Parallel phone 5s -->${speed}`);
        done();
        // do anthor something ..
    },5000)
})
student.hooks.run.tapAsync('otherDevice',(speed,done)=>{
    setTimeout(()=>{
        console.info(`Async Parallel otherDevice 3s -->${speed}`);
        done();
        // do anthor something ..
    },3000)
})

student.hooks.run.tapAsync('SportsBracelet',(speed,done)=>{
    setTimeout(()=>{
        console.info(`Async Parallel SportsBracelet 2s -->${speed}`);
        done();
        // do anthor something ..
    },2000)
})



/**
 * Series
 */
student.hooks.runSeries.tapAsync('phone',(speed,done)=>{
    console.info(`Async Series  1, tapAsync`);
    setTimeout(()=>{
        console.info(`Async Series   phone 5s -->${speed}`);
        done();
        // do anthor something ..
    },5000)
})
student.hooks.runSeries.tapAsync('otherDevice',(speed,done)=>{
    console.info(`Async Series   2, tapAsync`);
    setTimeout(()=>{
        console.info(`Async Series   otherDevice 3s -->${speed}`);
        done();
        // do anthor something ..
    },3000)
})

student.hooks.runSeries.tapAsync('SportsBracelet',(speed,done)=>{
    console.info(`Async Series   3, tapAsync`);
    setTimeout(()=>{
        console.info(`Async Series   SportsBracelet 2s -->${speed}`);
        done();
        // do anthor something ..
    },2000)
})


/**
 * Promise
 */
student.hooks.runPromise.tapPromise('promisePhone',(speed)=>{
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.info(`Async SeriesWaterfall 2s -->${speed}`);
            resolve(speed);
            // do something ..
        },2000)
    })
})

student.runAsync('2km/h');
student.runSeries('2.3km/h');
student.runPromise(120);


#5 总结
  1. 我是第一次在源码当中看到这种字符串拼接的代码书写方式,给我带来了很大的启发,其中最大的收获:通过字符串的方式缓存每次编译的生成的 fn,能够减少内存占用情况,使得代码性能提升。
  2. 思考:拦截器的处理模块是否能够简化呢?洋葱模型可以简化它吗?又或者启发方式能够简化拦截器的处理?
#6 Flag

看完了tapable的事件流处理方式,突然想对比下node的events模块了,文章拖延了半个月,这次立下flag,下周(4.30)更新events源码