preface

The official Webpack documentation describes the Plugin API as follows:

Tapable, a small library, is a core tool for WebPack, but can be used elsewhere to provide a similar plug-in interface. Many objects in Webpack extend from the Tapable class. This class exposes the TAP, tapAsync, and tapPromise methods, which you can use to inject custom build steps that will be triggered at different times throughout the compilation process.

The body of the

Tapable exposed hooks can be divided into synchronous and asynchronous types, and these two types can be divided into parallel and serial modes in execution. Details are as follows:

The introduction of each Hook is as follows:

The serial number The name of the hook Implement way Use the point
1 SyncHook Synchronous serial Does not care about the return value of the listening function
2 SyncBailHook Synchronous serial If one of the listener functions returns a value of noundefined, all remaining logic is skipped
3 SyncWaterfallHook Synchronous serial The return value of the previous listener can be passed to the next listener
4 SyncLoopHook Synchronous cycle When fired, the listener is repeated if it returns true, and exits the loop if it returns undefined
5 AsyncParallelHook Asynchronous concurrent Does not care about the return value of the listening function
6 AsyncParallelBailHook Asynchronous concurrent As long as the return value of the listener function is notundefined, ignores subsequent listener function execution, jumps directly to callAsync and other callback functions that trigger the function binding, and then executes the bound callback function
7 AsyncSeriesHook Asynchronous serial port Does not relate to arguments to callback()
8 AsyncSeriesBailHook Asynchronous serial port The argument to callback() is notundefinedCallback functions such as callAsync that trigger the function binding are executed directly
9 AsyncSeriesWaterfallHook Asynchronous serial port The second argument to the callback(err, data) in the previous listener can be used as an argument to the next listener

example

class Car {
	constructor() { this.hooks = { accelerate: new SyncHook(["newSpeed"])}; }// Declare the hook

	// The hook call
	setSpeed(newSpeed) { this.hooks.accelerate.call(newSpeed); }}// Declare an instance
const myCar = new Car();

// Register event 1
myCar.hooks.accelerate.tap("LoggerPlugin".newSpeed= > console.log(`[LoggerPlugin]Accelrating to ${newSpeed}`));

// Register event 2
myCar.hooks.accelerate.tap(
	{ name: "LoggerPlugin2".before: "LoggerPlugin" },
	newSpeed= > console.log(`[LoggerPlugin1]Accelrating to ${newSpeed}`));// Register event 3
myCar.hooks.accelerate.tap({
	name: "LoggerPlugin3".fn: newSpeed= > console.log(`[LoggerPlugin3]Accelrating to ${newSpeed}`)}); myCar.hooks.accelerate.intercept({call: newSpeed= > console.log(`The newSpeed is: ${newSpeed}`),
	tap: tapInfo= > console.log(`${tapInfo.name}tap was trigged! `),
	register: tapInfo= > {
		console.log(`${tapInfo.name} is doing its jos`.JSON.stringify(tapInfo));
		returntapInfo; }}); myCar.setSpeed(60);
Copy the code

The result is as follows:

LoggerPlugin2 is doing its jos {"type":"sync"."name":"LoggerPlugin2"."before":"LoggerPlugin"}
LoggerPlugin is doing its jos {"type":"sync"."name":"LoggerPlugin"}
LoggerPlugin3 is doing its jos {"type":"sync"."name":"LoggerPlugin3"}
The newSpeed is: 60
LoggerPlugin2 tap was trigged!
[LoggerPlugin1]Accelrating to 60
LoggerPlugin tap was trigged!
[LoggerPlugin]Accelrating to 60
LoggerPlugin3 tap was trigged!
[LoggerPlugin3]Accelrating to 60
Copy the code

Specifically, the use of Tapable Hook can be roughly divided into three steps.

  1. Var sync = new SyncHook([‘a’, ‘b’])
  2. Register several events sync.tap(‘name’, fn)
  3. Call the event registered in step 2 through sync.call(a, b) (_createCall generates call execution logic only when a call is called)

The generated Call method is shown at the end of this article.

The source code interpretation

The core code is distributed in two files: Hook. Js and HookcodeFactory.js. The former mainly implements the logic for registering events to provide a base class from which other hooks can inherit, while the latter provides the logic for generating the call, callAsync, and callPromise method bodies to generate the logic for performing the registration method create.

We use the SyncHook used in the above example to read the source code. Other Hook implementations are pretty much the same.

SyncHook

The core code is as follows

class SyncHookCodeFactory extends HookCodeFactory {
	// Use it in HookCodeFactory contentWithInterceptors to generate the execution logic
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) = >onError(err), onDone, rethrowIfPossible }); }}const factory = new SyncHookCodeFactory();

// Generate the method body that executes the logic
const COMPILE = function(options) {
	Setup, see HookCodeFactory for the create method
	factory.setup(this, options);
	return factory.create(options);
};

function SyncHook(args = [], name = undefined) {
	// Use Hook to initialize a base Hook
	const hook = new Hook(args, name);
	hook.constructor = SyncHook;
	hook.tapAsync = TAP_ASYNC;
	hook.tapPromise = TAP_PROMISE;
	hook.compile = COMPILE;
	return hook;
}
Copy the code

A few key points:

  1. new Hook()Used to initialize an underlying Hook instance
  2. COMPILEEncapsulate the generationcallPerforms the logic of the code in the callcallWill be called (see belowCALL_DELEGATE)
  3. contentMethod is used during executionCOMPILESome of the methodscustomLogic.

Hook.js

Core code:

const CALL_DELEGATE = function(. args) {
	this.call = this._createCall("sync");
	return this.call(... args); };class Hook {
	constructor(args = [], name = undefined) {
        // omit code...
        this.taps = [];
        this.call = CALL_DELEGATE;
	}
	Compile logic is not implemented here. Instead, compile is assigned in each Hook class to generate the execution code in HookCodeFactory
    // Refer to COMPILE in AsyncHook above
	compile(options) {
		throw new Error("Abstract: should be overridden");
	}

	_createCall(type) {
		return this.compile({ // omit code...
        });
	}

	_tap(type, options, fn) {
		// omit code...
		options = Object.assign({ type, fn }, options);
        Register in the _runRegisterInterceptors interceptor can customize options and return the new configuration
		options = this._runRegisterInterceptors(options);
		this._insert(options);
	}
    
	// Register events
	tap(options, fn) {
		this._tap("sync", options, fn);
	}

	// Add a new declaration method to the TAPS event queue and adjust the execution order of the registration methods,
	_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

Key points:

  1. tapMethod for registering normal events (tapAsync, tapPromise for registering asynchronous events)
  2. Core method_tapWill be called_insertAdd events to the registered event queue and adjust the order in which they are executed (if passed in while registering the event)stage.beforeParameter).

In _insert, it is difficult to adjust the execution order of events, you can see the comments added in the author’s reading

HookCodeFactory.js

The core is the create method. The specific logic is to concatenate string through new Function and generate the method body of the pass parameter and call method. You can learn the code to download to the local, do it yourself.

myCar.hooks.accelerate.call

newSpeed => {
	var _context;
	var _x = this._x;
	var _taps = this.taps;
	var _interceptors = this.interceptors;
	_interceptors[0].call(newSpeed);
	var _tap0 = _taps[0];
	_interceptors[0].tap(_tap0);
	var _fn0 = _x[0];
	_fn0(newSpeed);
	var _tap1 = _taps[1];
	_interceptors[0].tap(_tap1);
	var _fn1 = _x[1];
	_fn1(newSpeed);
	var _tap2 = _taps[2];
	_interceptors[0].tap(_tap2);
	var _fn2 = _x[2];
	_fn2(newSpeed);
};
Copy the code

Want to say

Although the core code will be registered and implemented to do a separation, to achieve the decoupling of each Hook, but increased the difficulty of understanding, in addition to the specific implementation of create method feel there is room for optimization, the current splicing way undoubtedly increases the cost of memory, except the big man…

Improper place, also hope to point out!!

  • Webpack Chinese document
  • Tapable Github
  • Webpack4.0 source code analysis Tapable
  • Here’s what you need to know about Tapable