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;