The Tapable SyncHook

Implement way

Synchronous serial, that is, the return value of each monitoring function has no correlation, and there is no return value of a function as the input parameter of B function, belonging to basicHook

The use of the SyncHook

const { SyncHook } = require('.. /.. /lib')
const queue = new SyncHook(['param1'])
queue.tap('event 1'.function (param1) {
    console.log(param1, '1')
})
queue.tap('event 2'.function (param2) {
    console.log(param2, '2')
})
queue.tap('event 3'.function (param3) {
    console.log(param3, '3')
})
queue.call('hello world')
// hello world 1
// hello world 2
// hello world 3
Copy the code

The realization of the SyncHook

class SyncHook {
    constructor() {
        this.taps = []
    }
    tap (name, fn) {
        this.taps.push(fn)
    }
    call() {
        this.taps.forEach(fn= >fn(... arguments)) } }Copy the code

SyncHook source code interpretation

First take a look at the entry file syncook.js

"use strict";
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);
		returnfactory.create(options); }}module.exports = SyncHook
Copy the code

SyncHook inherits from the base Hook class. Before you look at the Hook code, see tapAsync and tapPromise here, and you can see that SyncHook does not support asynchronous binding time and callbacks. The compile method should override compile inside the Hook.

Let’s look at the internal implementation of the base Hook class

Take a look at the Hook constructor first

	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;
	}
Copy the code

Taps, call, _args are initialized when we new SyncHook([‘param1’])

Queue. Tap goes to the tap method

	tap(options, fn) {
		if (typeof options === "string") options = { name: options };
		if (typeofoptions ! = ="object" || options === null)
			throw new Error(
				"Invalid arguments to tap(options: Object, fn: function)"
			);
		options = Object.assign({ type: "sync".fn: fn }, options);
		if (typeofoptions.name ! = ="string" || options.name === "")
			throw new Error("Missing name for tap");
		options = this._runRegisterInterceptors(options);
		this._insert(options);
	}
Copy the code

Indicating that tap’s first input is either a string or an object, tap passes the constructed Options object to the _INSERT method

	_insert(item) {
		this._resetCompilation();
		let 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;
			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;
	}
Copy the code

Because SyncHook’s tap registers as a string, it doesn’t care about the logic associated with before; In this case, it is a push process. In this case, it is equivalent to passing the options constructed above as parameters to this. Taps

So what happens when I queue. Call

_resetCompilation() {
  this.call = this._call;
  this.callAsync = this._callAsync;
  this.promise = this._promise;
}
Copy the code

We can see that the call method is initialized both at the initial stage of _insert and in the constructor

Object.defineProperties(Hook.prototype, {
	_call: {
		value: createCompileDelegate("call"."sync"),
		configurable: true.writable: true
	},
	_promise: {
		value: createCompileDelegate("promise"."promise"),
		configurable: true.writable: true
	},
	_callAsync: {
		value: createCompileDelegate("callAsync"."async"),
		configurable: true.writable: true}});Copy the code

This. Call = this._call createCompileDelegate(“call”, “sync”)

And then we look down

function createCompileDelegate(name, type) {
	return function lazyCompileHook(. args) {
		this[name] = this._createCall(type);
		return this[name](... args); }; }Copy the code

You can see that the this.call method has been redefined with the this._createCall method

_createCall(type) {
  return this.compile({
    taps: this.taps,
    interceptors: this.interceptors,
    args: this._args,
    type: type
  });
}
compile(options) {
  throw new Error("Abstract: should be overriden");
}
Copy the code

The compile method here is actually overwritten in syncook.js

compile(options) {
  factory.setup(this, options);
  return factory.create(options);
}
Copy the code

At this point we need to look inside the SyncHookCodeFactory

setup(instance, options) {
	instance._x = options.taps.map(t= > t.fn);
}
Copy the code
	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;
			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;
	}
Copy the code

You can see that setup is extracted by the options callback and placed on _x. In the create method, different types are passed and different strings are concatenated to be executed by the new Function

The content method is implemented in syncook.js

class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) = >onError(err), onDone, rethrowIfPossible }); }}Copy the code
callTapsSeries({ onError, onResult, resultReturns, onDone, doneReturns, rethrowIfPossible }) {
	if (this.options.taps.length === 0) return onDone();
	const firstAsync = this.options.taps.findIndex(t= >t.type ! = ="sync");
	const somethingReturns = resultReturns || doneReturns || false;
	let code = "";
	let current = onDone;
	for (let j = this.options.taps.length - 1; j >= 0; j--) {
		const i = j;
		constunroll = current ! == onDone &&this.options.taps[i].type ! = ="sync";
		if (unroll) {
			code += `function _next${i}() {\n`;
			code += current();
			code += `}\n`;
			current = () = > `${somethingReturns ? "return " : ""}_next${i}(a); \n`;
		}
		const done = current;
		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);
				}),
			onDone: !onResult && done,
			rethrowIfPossible:
				rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
		});
		current = () = > content;
	}
	code += current();
	return code;
}
Copy the code
	callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
		let code = "";
		let hasTapCached = false;
		for (let i = 0; i < this.options.interceptors.length; i++) {
			const interceptor = this.options.interceptors[i];
			if (interceptor.tap) {
				if(! hasTapCached) { code +=`var _tap${tapIndex} = The ${this.getTap(tapIndex)}; \n`;
					hasTapCached = true;
				}
				code += `The ${this.getInterceptor(i)}.tap(${
					interceptor.context ? "_context, " : ""
				}_tap${tapIndex}); \n`;
			}
		}
		code += `var _fn${tapIndex} = The ${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) {
					code += `var _result${tapIndex} = _fn${tapIndex}(The ${this.args({
						before: tap.context ? "_context" : undefined
				})}); \n`;
				} else {
					code += `_fn${tapIndex}(The ${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":
				let cbCode = "";
				if (onResult) cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;
				else cbCode += `_err${tapIndex} => {\n`;
				cbCode += `if(_err${tapIndex}) {\n`;
				cbCode += onError(`_err${tapIndex}`);
				cbCode += "} else {\n";
				if (onResult) {
					cbCode += onResult(`_result${tapIndex}`);
				}
				if (onDone) {
					cbCode += onDone();
				}
				cbCode += "}\n";
				cbCode += "}";
				code += `_fn${tapIndex}(The ${this.args({
					before: tap.context ? "_context" : undefined,
					after: cbCode
				})}); \n`;
				break;
			case "promise":
				code += `var _hasResult${tapIndex}= false; \n`;
				code += `var _promise${tapIndex} = _fn${tapIndex}(The ${this.args({
					before: tap.context ? "_context" : undefined
				})}); \n`;
				code += `if (! _promise${tapIndex}| |! _promise${tapIndex}.then)\n`;
				code += `  throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex}+ ') '); \n`;
				code += `_promise${tapIndex}.then(_result${tapIndex} => {\n`;
				code += `_hasResult${tapIndex}= true; \n`;
				if (onResult) {
					code += onResult(`_result${tapIndex}`);
				}
				if (onDone) {
					code += onDone();
				}
				code += `}, _err${tapIndex} => {\n`;
				code += `if(_hasResult${tapIndex}) throw _err${tapIndex}; \n`;
				code += onError(`_err${tapIndex}`);
				code += "}); \n";
				break;
		}
		return code;
	}

Copy the code

For SyncHook, the focus is on two lines of code at a and B

a: code += `var _fn${tapIndex} = The ${this.getTapFn(tapIndex)}; \n`;
Copy the code
getTapFn(idx) {
	return `_x[${idx}] `;
}
Copy the code
b: code += `_fn${tapIndex}(The ${this.args({
	before: tap.context ? "_context" : undefined
})}); \n`;
Copy the code
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(","); }}Copy the code

The _args is the args passed in by _createCall, which is the argument passed in by new SyncHook

This is the main process of concatenating code strings, resulting in the following string:

var _fn2 = _x[2]; // Output of a
_fn2(param1); // output of b
var _fn1 = _x[1];
_fn1(param1);
var _fn2 = _x[2];
_fn2(param1);
Copy the code

Once you have the string above, hand it to the new Function to execute;

The above is Tapable-SyncHook usage, implementation and source code analysis;