preface

This article officially entered the source code interpretation.

Overall, Tapable source code is not complex, the core of the two classes Hook and HookCodeFactory.

Taking SyncHook as an example, let’s take a look at its internal implementation, gradually introducing the internal implementation logic of Hook and HookCodeFactory.

Finally, two helper classes, HookMap and MultiHook.

The source code

Take SyncHook as an example and look at its implementation.

SyncHook

The implementation of this class is in the tapable /lib/syncook.js file.

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");
};

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

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;

module.exports = SyncHook;
Copy the code

The overall logical

  1. Define the SyncHookCodeFactory class and inherit from HookCodeFactory, which is a class that generates executable code
  2. Next, instantiate the SyncHookCodeFactory
  3. Then define the TAP_ASYNC, TAP_PROMISE, and COMPILE methods. Because SyncHook is synchronous and does not have asynchronous subscriptions, an error is thrown when using tapAsync and tapPromise subscriptions
  4. Define a synchronous SyncHook function and export it

SyncHookMethod internal logic

  1. Instantiate a Hook with a new Hook
  2. Then modify the constructor object of the instantiated object
  3. Override the tapAsync and tapPromise methods for instantiating objects
  4. Override the compile method of instantiated objects
  5. Returns the instantiated object

Let’s ignore the Hook and HookCodeFactory classes, the logic of the entire file is a complete routine.

The other hooks are implemented in the same way, the only difference being the implementation of the classes that inherit the HookCodeFactory and their respective Content methods.

Such as AsyncSeriesLoopHook implementation is as follows: the code in a file tapable/lib/AsyncSeriesLoopHook js

const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class AsyncSeriesLoopHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone }) {
		return this.callTapsLooping({
			onError: (i, err, next, doneBreak) = > onError(err) + doneBreak(true), onDone }); }}const factory = new AsyncSeriesLoopHookCodeFactory();

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

function AsyncSeriesLoopHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	hook.constructor = AsyncSeriesLoopHook;
	hook.compile = COMPILE;
	hook._call = undefined;
	hook.call = undefined;
	return hook;
}

AsyncSeriesLoopHook.prototype = null;

module.exports = AsyncSeriesLoopHook;
Copy the code

AsyncSeriesLoopHook is an asynchronous method, so resetting the call and _call methods on its instantiated object means it cannot be called using the call method.

Identify two points from the source code

Looking at the source code can determine two points:

  1. Synchronous hooks cannot be subscribed with tapAsync and tapPromise because these two methods are reset to:
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");
};
Copy the code
  1. An asynchronous Hook cannot be called through the call method because it is reset to:
hook._call = undefined;
hook.call = undefined;
Copy the code

Hook type

This class is defined in the tapable /lib/hook.js file.

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); };class Hook {
	constructor(args = [], name = undefined) {
		this._args = args;
		this.name = name;
		this.taps = [];
		this.interceptors = [];
		this._call = CALL_DELEGATE;
		this.call = CALL_DELEGATE;
		this._callAsync = CALL_ASYNC_DELEGATE;
		this.callAsync = CALL_ASYNC_DELEGATE;
		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;
	}

	compile(options) {
		throw new Error("Abstract: should be overridden");
	}

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

	_tap(type, options, fn) {
		if (typeof options === "string") {
			options = {
				name: options.trim()
			};
		} else if (typeofoptions ! = ="object" || options === null) {
			throw new Error("Invalid tap options");
		}
		if (typeofoptions.name ! = ="string" || options.name === "") {
			throw new Error("Missing name for tap");
		}
		if (typeofoptions.context ! = ="undefined") {
			deprecateContext();
		}
		options = Object.assign({ type, fn }, options);
		options = this._runRegisterInterceptors(options);
		this._insert(options);
	}

	tap(options, fn) {
		this._tap("sync", options, fn);
	}

	tapAsync(options, fn) {
		this._tap("async", options, fn);
	}

	tapPromise(options, fn) {
		this._tap("promise", options, fn);
	}

	_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) {
		const mergeOptions = opt= >
			Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);

		return {
			name: this.name,
			tap: (opt, fn) = > this.tap(mergeOptions(opt), fn),
			tapAsync: (opt, fn) = > this.tapAsync(mergeOptions(opt), fn),
			tapPromise: (opt, fn) = > this.tapPromise(mergeOptions(opt), fn),
			intercept: interceptor= > this.intercept(interceptor),
			isUsed: () = > this.isUsed(),
			withOptions: opt= > this.withOptions(mergeOptions(opt))
		};
	}

	isUsed() {
		return this.taps.length > 0 || this.interceptors.length > 0;
	}

	intercept(interceptor) {
		this._resetCompilation();
		this.interceptors.push(Object.assign({}, interceptor));
		if (interceptor.register) {
			for (let i = 0; i < this.taps.length; i++) {
				this.taps[i] = interceptor.register(this.taps[i]); }}}_resetCompilation() {
		this.call = this._call;
		this.callAsync = this._callAsync;
		this.promise = this._promise;
	}

	_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

In the constructor, we define the tap, tapAsync, and tapPromise methods, which are the methods we use when we subscribe to the function.

All three methods call the this._tap private method on the prototype chain, the only difference being that the first arguments sync, Async, and PROMISE represent different subscription function types.

_tap method

_tap(type, options, fn) {
	if (typeof options === "string") {
		options = {
			name: options.trim()
		};
	} else if (typeofoptions ! = ="object" || options === null) {
		throw new Error("Invalid tap options");
	}
	if (typeofoptions.name ! = ="string" || options.name === "") {
		throw new Error("Missing name for tap");
	}
	if (typeofoptions.context ! = ="undefined") {
		deprecateContext();
	}
	options = Object.assign({ type, fn }, options);
	options = this._runRegisterInterceptors(options);
	this._insert(options);
}
Copy the code

The main function of the private _TAP method is to serialize options and add them to the TAPS array.

Implementation logic:

  1. Check the type of the options parameter. Options is ultimately an object and must have the name attribute
  2. If options has a context property, the deprecateContext method is called, indicating that the context property is about to be discarded
  3. Call the assign method to merge options so that it has the name, type, and FN attributes
  4. Call the private _runRegisterInterceptors method to modify options
  5. The private method _insert is called to adjust the order of the subscription functions stored in the TAPS array

_runRegisterInterceptors method

_runRegisterInterceptors(options) {
	for (const interceptor of this.interceptors) {
		if (interceptor.register) {
			const newOptions = interceptor.register(options);
			if(newOptions ! = =undefined) { options = newOptions; }}}return options;
}
Copy the code

The main function of the private _runRegisterInterceptors method is to modify the Options object.

Implementation logic

  • Iterates through all interceptors, calling the register property method if it has one, passing the original options as an argument, and modifying options if it does not return undefined. The options modified by the last interceptor register is the final options

The register property methods of all interceptors have the ability to modify the Tap object of the subscription function.

_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

The main function of the private method _run is to adjust the order of subscription functions based on the value of before or stage, which is ultimately stored in the TAPS array.

Implementation logic

  1. Let’s convert before to Set
  2. Check that stage is assigned to the variable stage if it is of a numeric type
  3. fromtapsStart traversal at the end
    1. Let I –
    2. Get the value x of position I
    3. I’m assigning x to I plus 1, so I’m moving x back one position
    4. Get the stages of x
    5. If you havebefore
      1. Determine if x’s name exists before, i.e. the current item to be inserted before X, continue, and then execute while
    6. If there is no before, determine whether the stage of x is greater than the stage of item, and continue if it is
    7. I++ to close the loop, and that’s where the index should be inserted
  4. Put item in place of I

Here’s the logic:

Call, callAsync, and Promise methods

All three methods call the _createCall private method and then reassign the return value to the corresponding method.

Call method for example

this.call = CALL_DELEGATE;
Copy the code
const CALL_DELEGATE = function(. args) {
	this.call = this._createCall("sync");
	return this.call(... args); };Copy the code

_createCall method

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

This method calls the compile method.

Back at the beginning of the article, each Hook class overwrites the compile method after instantiation, which goes into HookCodeFactory logic.

HookCodeFactory class

The sample code

Let’s take a look at how the HookCodeFactory logic is entered internally using the following example.

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

const hook = new SyncHook(['name'.'age']);

hook.tap({
  name: 'js'
}, (name, age) = > {
  console.log('js', name, age);
});
hook.tap({
  name: 'css'
}, (name, age) = > {
  console.log('css', name, age);
});
hook.tap({
  name: 'node'
}, (name, age) = > {
  console.log('node', name, age);
});

hook.call('naonao'.2);
Copy the code

In the previous article has been the implementation of hook. Tap, source code analysis.

Let’s look at hook.call execution.

In the class Hook constructor there are:

This call is defined

this.call = CALL_DELEGATE;
Copy the code

CALL_DELEGATE definition

const CALL_DELEGATE = function(. args) {
	this.call = this._createCall("sync");
	return this.call(... args); };Copy the code

_createCall definition:

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

That what time is it

This place makes a few points:

  1. Taps stores something like this:
[{type: 'sync'.fn: f, name: 'js'},
    {type: 'sync'.fn: f, name: 'css'},
    {type: 'sync'.fn: f, name: 'node'}]Copy the code
  1. This. Interceptors stores an array of interceptor related objects
  2. This._args is the args passed in when the class is instantiated, like this:
['name'.'age']
Copy the code
  1. Type is one of sync, Async, or Promise

This.com running the definition

The this.pile method is overridden after each Hook is instantiated.

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

function SyncHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	// omit some code...
	hook.compile = COMPILE;
	return hook;
}
Copy the code

COMPILEInternal logic

  1. Call the setup method
  2. Call the create method and return

The result returned by the create method is eventually assigned to this.call of the CALL_DELEGATE method, which is then executed.

This completes the execution logic of hook. Call.

Let’s look at the setup and create methods executed inside the COMPILE method.

setup

The setup method is defined in HookCodeFactory as follows:

setup(instance, options) {
	instance._x = options.taps.map(t= > t.fn);
}
Copy the code

It maps taps, pulls out fn, and assigns a value to the _x property of the current Hook instance.

create

The create method is defined in HookCodeFactory as follows:

create(options) {
	this.init(options);
	let fn;
	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();
	return fn;
}
Copy the code

It concatenates different code strings based on type and creates a Function via new Function, assigns a value to fn, and returns fn.

So, previously, printing out the hook. Call method was completely transparent about the order and flow of subscription functions.

sync

Type is the case branch of sync:

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

The main logic is as follows:

  1. Call the this.args method to get the function parameters
  2. The string that starts with “use strict” is the body of the concatenation function

Enclosing the args

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

Concatenate the this._args array into a string according to **, **

This header method

header() {
	let code = "";
	if (this.needContext()) {
		code += "var _context = {}; \n";
	} else {
		code += "var _context; \n";
	}
	code += "var _x = this._x; \n";
	if (this.options.interceptors.length > 0) {
		code += "var _taps = this.taps; \n";
		code += "var _interceptors = this.interceptors; \n";
	}
	return code;
}
Copy the code
The main logic
  1. Call the this.needContext method to determine whether a context object is needed. Define context as an empty object if necessary. The taps function determines whether any member in this. Taps has context: true configured, i.e. {type: ‘sync’, fn: f, name: ‘js’, context: true}.
  2. Define cache _x is this._x, which is an array of the subscription function fn
  3. Check if there are interceptors, if there are _TAPS and _interceptors that define cached responses.
  4. Returns the concatenated string code

Enclosing contentWithInterceptors method

contentWithInterceptors(options) {
	if (this.options.interceptors.length > 0) {
		const onError = options.onError;
		const onResult = options.onResult;
		const onDone = options.onDone;
		let code = "";
		for (let i = 0; i < this.options.interceptors.length; i++) {
			const interceptor = this.options.interceptors[i];
			if (interceptor.call) {
				code += `The ${this.getInterceptor(i)}.call(The ${this.args({
					before: interceptor.context ? "_context" : undefined
				})}); \n`;
			}
		}
		code += this.content(
			Object.assign(options, {
				onError:
					onError &&
					(err= > {
						let code = "";
						for (let i = 0; i < this.options.interceptors.length; i++) {
							const interceptor = this.options.interceptors[i];
							if (interceptor.error) {
								code += `The ${this.getInterceptor(i)}.error(${err}); \n`;
							}
						}
						code += onError(err);
						return code;
					}),
				onResult:
					onResult &&
					(result= > {
						let code = "";
						for (let i = 0; i < this.options.interceptors.length; i++) {
							const interceptor = this.options.interceptors[i];
							if (interceptor.result) {
								code += `The ${this.getInterceptor(i)}.result(${result}); \n`;
							}
						}
						code += onResult(result);
						return code;
					}),
				onDone:
					onDone &&
					(() = > {
						let code = "";
						for (let i = 0; i < this.options.interceptors.length; i++) {
							const interceptor = this.options.interceptors[i];
							if (interceptor.done) {
								code += `The ${this.getInterceptor(i)}.done(); \n`;
							}
						}
						code += onDone();
						returncode; })}));return code;
	} else {
		return this.content(options); }}Copy the code
  1. First of all, there is no logic without an interceptor, else logic, which returns this.content directly.

This. content is defined in

class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) = >onError(err), onDone, rethrowIfPossible }); }}Copy the code

You can see that it calls this.callTapsSeries and returns the result.

  1. Take a look at the situation with interceptors:
    1. For loops through all interceptors and concatenates a string for each interceptor to execute the Call method if the call property method is configured
    2. The this.content method is still executed, except that the arguments are the result of the original options and the concatenation of each error, result, and done executable code string on the interceptor

callTapsSeries

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;
	let code = "";
	let current = onDone;
	let unrollCounter = 0;
	for (let j = this.options.taps.length - 1; j >= 0; j--) {
		const i = j;
		constunroll = current ! == onDone && (this.options.taps[i].type ! = ="sync" || unrollCounter++ > 20);
		if (unroll) {
			unrollCounter = 0;
			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

The main function is to traverse TAPS in reverse order, ignoring asynchronous cases first.

Focus on the this.callTap section, which assigns the return result to the content and then resets current to () => content; This takes advantage of the closure’s properties to cache the string that was last concatenated.

Each time the loop assigns current to done, the done method is executed inside the this.callTap call, concatenating its results in place.

Finally return the concatenated string code.

callTap

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 += `(function(_err${tapIndex}, _result${tapIndex}) {\n`;
			else cbCode += `(function(_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((function(_result${tapIndex}) {\n`;
			code += `_hasResult${tapIndex}= true; \n`;
			if (onResult) {
				code += onResult(`_result${tapIndex}`);
			}
			if (onDone) {
				code += onDone();
			}
			code += `}), function(_err${tapIndex}) {\n`;
			code += `if(_hasResult${tapIndex}) throw _err${tapIndex}; \n`;
			code += onError(`_err${tapIndex}`);
			code += "}); \n";
			break;
	}
	return code;
}
Copy the code

It begins by traversing the interceptor and concatenating its execution string code if it has a TAP property method.

So each subscription function is executed by executing the interceptor’s TAP method.

Var _fn${tapIndex} var _fn0 = _x[0]

Then determine the current tap type and enter the different branches.

Take a look at the Sync branch here.

  1. If rethrowIfPossible is false, concatenate ** _hasError** and try
  2. In the case of onResult, the result of the concatenation of whether the subscription function needs to be cached is __result, because some hooks need to proceed based on the result
  3. Again, in the case of rethrowIfPossible, concatenate the catch part, so try… Catch is complete, and step 2 is just try
  4. Then, according to the case of onResult, pass the cached execution result as a parameter, concatenate if… The else. For example, SyncBailHook’s onResult is:
class SyncBailHookCodeFactory extends HookCodeFactory {
	content({ onError, onResult, resultReturns, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) = > onError(err),
			onResult: (i, result, next) = >
				`if(${result}! == undefined) {\n${onResult( result )}; \n} else {\n${next()}}\n`, resultReturns, onDone, rethrowIfPossible }); }}Copy the code

From this onResult we can concatenate the string code that determines that if the result is not undefined, next will not be executed, that is, it will not be executed down. 5. Execute onDone as current in callTapsSeries, which caches the string code from the last subscription function to perform concatenation 6. Finally, close the code block according to rethrowIfPossible

So that’s the whole splicing process.

Concatenated string code that eventually creates a new Function from new Function and assigns it to Call.

TapAsync and Promise are also concatenated according to this process, but the specific concatenated code is different. Refer to async and Promise in the branch for details.

HookMap

The HookMap class maps to a Hook based on a string. Internal is stored in a Map object according to the key, when used according to the specific key value can obtain the corresponding Hook.

class HookMap {
	constructor(factory, name = undefined) {
		this._map = new Map(a);this.name = name;
		this._factory = factory;
		this._interceptors = [];
	}

	get(key) {
		return this._map.get(key);
	}

	for(key) {
		const hook = this.get(key);
		if(hook ! = =undefined) {
			return hook;
		}
		let newHook = this._factory(key);
		const interceptors = this._interceptors;
		for (let i = 0; i < interceptors.length; i++) {
			newHook = interceptors[i].factory(key, newHook);
		}
		this._map.set(key, newHook);
		return newHook;
	}

	intercept(interceptor) {
		this._interceptors.push(
			Object.assign(
				{
					factory: defaultFactory }, interceptor ) ); }}Copy the code

These are the set and get operations on the Map.

MultiHook

A MultiHook is one that defines many hooks to be stored in an array. When using TAP or tapAsync or tapPromise, the group of numbers is traversed and the corresponding TAP or tapAsync or tapPromise methods are called for each member.

This is to define a subscription function that can be distributed to many hooks.

class MultiHook {
	constructor(hooks, name = undefined) {
		this.hooks = hooks;
		this.name = name;
	}

	tap(options, fn) {
		for (const hook of this.hooks) { hook.tap(options, fn); }}tapAsync(options, fn) {
		for (const hook of this.hooks) { hook.tapAsync(options, fn); }}tapPromise(options, fn) {
		for (const hook of this.hooks) { hook.tapPromise(options, fn); }}isUsed() {
		for (const hook of this.hooks) {
			if (hook.isUsed()) return true;
		}
		return false;
	}

	intercept(interceptor) {
		for (const hook of this.hooks) { hook.intercept(interceptor); }}withOptions(options) {
		return new MultiHook(
			this.hooks.map(h= > h.withOptions(options)),
			this.name ); }}Copy the code

conclusion

Tapable source code analysis is finished, for callAsync and promise method concatenation process, interested in you can take a look.

Overall, Tapable’s source code is not complex, but it is the core of the Webpack plug-in, and the ideas are worth learning.

More exciting, please pay attention to the wechat public number: make a noise front end