Version 2.2.1 omits the context, Interceptors code.

Based on publish/subscribe

Subscribe (more features, such as parameter lists, interceptors, multiple synchronous/asynchronous hooks, multiple function execution processes).

const {SyncHook} = require('tapable')
const h1 = new SyncHook(["test"]);
h1.tap("C".(test) = >console.log(test));
h1.call("1");/ / print 1
Copy the code

SyncHook source

Tapable hooks, the architecture is consistent, I from SyncHook source code examples.

// Simplify the code, boundary processing code has been removed
function SyncHook(args = [], name = undefined) {
  // The core Hook class of all kinds of hooks
  // In charge of: tap(register) call(call)
	const hook = new Hook(args, name);
  Compile is implemented by the HookCodeFactory class
  // Be responsible for compiling (dynamically generating the hook. Call function)
	hook.compile = COMPILE;
	return hook;
}
// Implement polymorphism by subclass
// Improve the flexibility of hooks
class SyncHookCodeFactory extends HookCodeFactory {
  // Generate the call function
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) = >onError(err), onDone, rethrowIfPossible }); }}const factory = new SyncHookCodeFactory();

const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};
Copy the code

Start with the Hook class

Hook type

constructo

class Hook {
  constructor(args = [], name = undefined) {
    // Passable parameters in the callback
    // eg: new SyncHook(["test", "arg2", "arg3"]);
    // The callback function can be passed
		this._args = args;
		this.name = name;
    // tap register function
		this.taps = [];
    // Three callback types execute call
		this.call = CALL_DELEGATE;
		this.callAsync = CALL_ASYNC_DELEGATE;
		this.promise = PROMISE_DELEGATE;
    // There are three ways to register
		this.tap = this.tap;
		this.tapAsync = this.tapAsync;
		this.tapPromise = this.tapPromise;
		this._x = undefined; }}Copy the code

TAB method

The tap method, which is a wrapper around _tap, is used to distinguish between callback types

// Options is usually string
/ / is the tap (name, callback)
class Hook{
  tap(options, fn) { 
    this._tap("sync", options, fn);
  }
  tapAsync(options, fn) {
    this._tap("async", options, fn);
  }
  tapPromise(options, fn) {
    this._tap("promise", options, fn); }}Copy the code

_tap

Tap into this. Taps.

class Hook {
  _tap(type, options, fn) {
    if (typeof options === "string") {
      options = { name: options.trim()};
    }
    options = Object.assign({ type, fn }, options);
    // The interceptor executes register
    options = this._runRegisterInterceptors(options);
    // Insert listener options
    // This. Taps. Push [options]
    this._insert(options);
    // options values: {type: "sync", fn: Function, name: "A"}}}Copy the code

The TAP process ends.

Call method

Create call, execute call(… args)

// Call is assigned to Hook constructor
// There are three kinds of call methods
class Hook{
  constructor(args = [], name = undefined) {
      this.call = CALL_DELEGATE;
      this.callAsync = CALL_ASYNC_DELEGATE;
      this.promise = PROMISE_DELEGATE; }}const CALL_DELEGATE = function(. args) {
	this.call = this._createCall("sync");
	return this.call(... args); };const CALL_ASYNC_DELEGATE = function(. args) {
	this.callAsync = this._createCall("async");
	return this.callAsync(... args); };const PROMISE_DELEGATE = function(. args) {
	this.promise = this._createCall("promise");
	return this.promise(... args); };Copy the code

All three calls are wrappers for _createCall.

_createCall

To call this.pile, a subclass must override this method.

class Hook{
	_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"); }}Copy the code

compile

Take SyncHook, for example

// Simplify arranging code in thought order
function SyncHook(args = [], name = undefined) {
  const hook = new Hook(args, name);
  / / rewrite the compile
	hook.compile = COMPILE;
	return hook;
}

Call (tap); // Call (tap); The following fine speak
const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};

const factory = new SyncHookCodeFactory();

// Compile is done by subclasses of HookCodeFactory
class SyncHookCodeFactory extends HookCodeFactory {
  // Process the contents of compile's call function, according to what logic is constructed.
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) = >onError(err), onDone, rethrowIfPossible }); }}Copy the code

Hook.callThe flow chart



So let’s look at thatHookCodeFactorySource code, how to dynamically generatecall.

HookCodeFactory class

You can tell by the name, this generates code. Function: Taps traverses taps, executes logic and generates different call functions according to the callback type. (Concatenate string new Function)

Start with compile for SyncHook

const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};
Copy the code

Start with Setup.

setup

Extract all functions registered with tap.

class HookCodeFactory {
  setup(instance, options) {
    // Taps functions are stored in _x.
		instance._x = options.taps.map(t= >t.fn); }}Copy the code

create

Build the call function dynamically. We only care about type:’sync’.

class HookCodeFactory {
  / / initialization
  init(options) {
		this.options = options;
    / / get the args eg: new SyncHook ([" test ", "arg2", "arg3]");
    ["test", "arg2", "arg3"]
		this._args = options.args.slice();
	}
  / / create
	create(options) {
		this.init(options);
    
		let fn;
    // Distinguish between callback types
    // Build the call function from a concatenated string
    // Describe this in more detail below
		switch (this.options.type) {
			case "sync":
				fn = new Function(
					this.args(),
					'"use strict"; \n' +
          this.header() +
          this.contentWithInterceptors({
            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.contentWithInterceptors({
            onError: err= > `_callback(${err}); \n`.onResult: result= > `_callback(null, ${result}); \n`.onDone: () = > "_callback(); \n"}));break;
			case "promise":
				let errorHelperUsed = false;
				const content = this.contentWithInterceptors({
					onError: err= > {
						errorHelperUsed = true;
						return `_error(${err}); \n`;
					},
					onResult: result= > `_resolve(${result}); \n`.onDone: () = > "_resolve(); \n"
				});
				let code = "";
				code += '"use strict"; \n';
				code += this.header();
				code += "return new Promise((function(_resolve, _reject) {\n";
				if (errorHelperUsed) {
					code += "var _sync = true; \n";
					code += "function _error(_err) {\n";
					code += "if(_sync)\n";
					code +=
						"_resolve(Promise.resolve().then((function() { throw _err; }))); \n";
					code += "else\n";
					code += "_reject(_err); \n";
					code += "}; \n";
				}
				code += content;
				if (errorHelperUsed) {
					code += "_sync = false; \n";
				}
				code += "})); \n";
				fn = new Function(this.args(), code);
				break;
		}
    
		this.deinit();
		returnfn; }}Copy the code

Concatenate string to generate call

Type == ‘sync’ in more detail.

// All examples are new SyncHook(["test", "arg2", "arg3"])
// Ignore context interceptors.

// (step 0)
fn = new Function(
  this.args(), // 'test, arg2, arg3'
  '"use strict"; \n' +
  this.header() + // var _x = this._x;
  this.contentWithInterceptors({ // var _fn0 = _x[0]; _fn0(test, arg2, arg3);
    onError: err= > `throw ${err}; \n`.onResult: result= > `return ${result}; \n`.resultReturns: true.onDone: () = > "".rethrowIfPossible: true}));// the final function of fn is as follows:
// function anonymous(test, arg2, arg3) {
// "use strict";
// var _context;
// var _x = this._x;
// var _fn0 = _x[0];
// _fn0(test, arg2, arg3);
// }

// Simplify the code
class HookCodeFactory {
  
  // step 1
  // Convert args to args string
  // eg: new SyncHook(["test", "arg2", "arg3"]); Output 'test, arg2, arg3'
	args(){
    let allArgs = this._args;
    return  allArgs.length === 0 ? "":allArgs.join(",");
  }
  
  // (step 2)
  // Variable declaration at the top of the code
  // Basically put _x(taps all registration functions) into functions.
  header() {
		let code = "";
    // omit context code
    code += "var _context; \n";
    // Put _x in the string
		code += "var _x = this._x; \n";
    // omit interceptors code
		return code;
	}
  
  // (step 3) The next step is in the bottom subclass
  // Add interceptors (not discussed in this article) in the execution of content
  contentWithInterceptors(options) {
    // omit interceptors code
    // Note: Content must have subclass overridden
    // There are overridden methods at the bottom
    return this.content(options);
	}
  
  // (step 3.2)
  // Synthesize the TAPS traversal of sync into a string of JS code that executes the TAP registration function.
  callTapsSeries({ onError, onResult, resultReturns, onDone, doneReturns, rethrowIfPossible }) {
		if (this.options.taps.length === 0) return onDone();
		let code = "";
		let current = onDone;
		for (let j = this.options.taps.length - 1; j >= 0; j--) {
			const i = j;
			const done = current;
      // (step 3.3)
      // this.callTap: Build a single tap into a JS code string
			const content = this.callTap(i, {
				onError: error= > onError(i, error, done),
				onResult:result= > onResult(i, result, done),
        onDone: !onResult && done,
			});
			current = () = > content;
		}
		code += current();
    // Example code: var _fn0 = _x[0]; _fn0(test, arg2, arg3);
		return code;
    // step(0)
	}
  
  // (step 3.3)
  // Convert tap registration function to js code string,
  callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
		let code = "";
		// omit the interceptors code
    Var _fn0 = _x[0]
		code += `var _fn${tapIndex} = _x[${tapIndex}]; \n`;
		const tap = this.options.taps[tapIndex];
		switch (tap.type) {
			case "sync":
        code += `_fn${tapIndex}(The ${this.args()}); \n`;
        // Example code: var _fn0 = _x[0]; _fn0(test, arg2, arg3);
				if (onDone) {
					code += onDone(); // onDone concatenates all tap strings.
				}
				break;
			case "async":
				// Skip over, the async class code logic also generates strings
				break;
			case "promise":
				// Skip over, the code logic for the Promise class also generates strings
				break;
		}
    // Example code: var _fn0 = _x[0]; _fn0(test, arg2, arg3);
		returncode; }}// Subclass overrides the content method
class SyncHookCodeFactory extends HookCodeFactory {
  // (step 3.1)
  / / the content method
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) = >onError(err), onDone, rethrowIfPossible }); }}Copy the code

The flow chart of HookCodeFactory

Tapable architecture diagram

conclusion

  • Hook: Responsible for subscription publishing.
    • tap(register function),call(Execution).
  • HookCodeFactory: responsible for compilation, dynamic generation forHookThe use ofcallFunction.
    • Distinguish betweenThe callback type:sync,async,promise. internalcompileThe function differentiates itself.
    • Distinguish betweenExecute the process: An inherited subclass, overridden by **content, shunt call
      • serialsync:callTapsSeries
      • cycleloop:callTapsLooping
      • parallelparallel:callTapsParallel

(Various hooks) : The logic above is aggregated to achieve different execution logic and callback types.

How to implement flexible subscription publishing?

  • A class implements subscription publishing.
  • A class implementation is flexible.
  • Then combine the two classes to improve the concrete logic.