Skip to content

tapable 事件流执行顺序

tapable 事件流的执行顺序是通过 注册时间、before、stage 三个来决定的

我们先看一个例子

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

// 创建一个同步事件流实例对象
const h1 = new SyncHook(['xxx', 'arg2']);

h1.intercept({
	call: (source, target, routesList) => {
		console.log('Starting to h1 routes');
	},
	register: tapInfo => {
		// tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ... }
		console.log(`${tapInfo.name} is doing its job`);
		return tapInfo; // may return a new tapInfo object
	},
	tap: tap => {},
});

// 添加同步事件A
h1.tap('A', function(args) {
	console.log('A', args);
	return 'b';
});

// 添加同步事件B
h1.tap('B', function() {
	console.log('b');
});

// 添加同步事件C
h1.tap('C', function() {
	console.log('c');
});
// 添加同步事件F
h1.tap(
	{
		name: 'F',
		before: 'D',
	},
	function() {
		console.log('F');
	}
);
// 添加同步事件E
h1.tap(
	{
		name: 'E',
		before: 'C',
	},
	function() {
		console.log('E');
	}
);
// 添加同步事件D
h1.tap('D', function() {
	console.log('D');
});

// 添加同步事件D
h1.tap(
	{
		name: 'G',
		stage: 10,
	},
	function() {
		console.log('G');
	}
);

// 添加同步事件D
h1.tap(
	{
		name: 'H',
		stage: 12,
	},
	function() {
		console.log('H');
	}
);

// 执行这个事件流
h1.call(7777);

然后我们看结果

js
A is doing its job
B is doing its job
C is doing its job
F is doing its job
E is doing its job
D is doing its job
G is doing its job
H is doing its job


Starting to h1 routes
F
A 7777
b
E
c
D
G
H

我们代码写入的顺序是 ABCFEDGH 但是执行的顺序却是:FABECDGH。具体为什么可以看 tapable 的 Hook.js 的 _insert()方法,然后通过 option.before 去循环遍历已有的事件流,如果 if(before.has(x.name)) 就删除一个,直到 before.size === 0,然后通过 option.stage 去判断前面的事件流 是否 if(xStage > stage),如果更大,那就继续向前。 可见事件流的顺序是按照 先判断是否 before 之前,然后再根据 option.stage 的大小,大的在前

js
class Hook {
	constructor(args) {
		if (!Array.isArray(args)) args = [];
		this._args = args;
		this.taps = [];
		this.interceptors = [];

		// 初始化的时候 通过函数柯里化 去生成 xx.call() 处理函数
		this.call = this._call = this._createCompileDelegate('call', 'sync');
		this.promise = this._promise = this._createCompileDelegate('promise', 'promise');
		this.callAsync = this._callAsync = this._createCompileDelegate('callAsync', 'async');
		this._x = undefined;
	}
	compile(options) {}
	_createCall(type) {}
	_createCompileDelegate(name, type) {}

	/**
	 * 添加事件流
	 * 
	 * 	
	   1. 添加同步事件B   option为字符串 直接作为事件的名称
			h1.tap('B', function () {
				console.log('b');
			});

		 2. 添加同步事件E   option为对象 name作为事件的名称
			h1.tap(
				{
					name: 'E',
					before: 'C',
				},
				function () {}
			);
	 * 
	 * 
	 */
	tap(options, fn) {
		// 处理第一个参数 支持字符串和对象类型 ,字符串直接作为事件名称;对象获取其中的name作为事件的名称 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);
	}

	tapAsync(options, fn) {
		if (typeof options === 'string') options = { name: options };
		if (typeof options !== 'object' || options === null)
			throw new Error('Invalid arguments to tapAsync(options: Object, fn: function)');
		options = Object.assign({ type: 'async', fn: fn }, options);
		if (typeof options.name !== 'string' || options.name === '') throw new Error('Missing name for tapAsync');
		options = this._runRegisterInterceptors(options);
		this._insert(options);
	}

	tapPromise(options, fn) {
		if (typeof options === 'string') options = { name: options };
		if (typeof options !== 'object' || options === null)
			throw new Error('Invalid arguments to tapPromise(options: Object, fn: function)');
		options = Object.assign({ type: 'promise', fn: fn }, options);
		if (typeof options.name !== 'string' || options.name === '') throw new Error('Missing name for tapPromise');
		options = this._runRegisterInterceptors(options);
		this._insert(options);
	}

	_runRegisterInterceptors(options) {
		for (const interceptor of this.interceptors) {
			if (interceptor.register) {
				const newOptions = interceptor.register(options);
				if (newOptions !== undefined) options = newOptions;
			}
		}
		return options;
	}
	withOptions(options) {}
	isUsed() {}
	intercept(interceptor) {}
	_resetCompilation() {}
	/**
	 * 添加一个事件流
	 *
	 *   此函数主要是通过 option.before 和 option.stage 来决定当前添加的事件流在队列中的位置
	 *
	 *
	 */
	_insert(item) {
		this._resetCompilation();
		// 处理before  支持  option.before = 'D' 也支持多个 option.before = ['D' ,'F' ,'D' ]
		let before;
		if (typeof item.before === 'string') before = new Set([item.before]);
		// 当然也支持数组的方式,去决定在多个事件之前
		else if (Array.isArray(item.before)) {
			before = new Set(item.before);
		}
		// 除了 before,option.stage权重 也决定事件流的位置
		let stage = 0;
		if (typeof item.stage === 'number') stage = item.stage;

		// 通过遍历原来的 this.taps中的数据,然后通过 before.has(x.name) 和 if(xStage > 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-- ,所以需要向后补一位
			i++;
			break;
		}
		// 将事件流保存到 对应的下标上
		this.taps[i] = item;
	}
}
module.exports = Hook;

从上面的代码中可以看出不管我们是通过 hook.tap()、hook.tapAsync()、hook.tapPromise()去注册对应的事件流,都是通过 hooks._insert()去真正的添加这个事件。

E 在 C 前很好解释 因为 name e 设置了 before c

哪为什么 F 在第一个?

json
{
	"name": "F",
	"before": "D"
}

其实很好解释 因为其设置了 before ,但是直到最后都没有找到 D 那么就只能放在第一个了, 所以如果其 before: [ 'D' , 'A' ] 其也会放在第一个。

上次更新: