preface

Webpack is a big name in front-end engineering, and there are two core objects in Webpack compilation.

  • The Compiler object responsible for the overall compilation process.
  • Compilation of Module Compilation objects.

In the Webpack world, there are two supporting eco-loaders and Plugin mechanisms.

If you are interested in learning about the Webpack Plugin, Tapable is a prerequisite for you.

The article will take you step by step to learn Tapable from use to principle. An article will take you to master Tapable thoroughly.

For more information on Webpack building principles and Loader mechanics, you can also read the previous Webpack column here: Playing with Webpack from Principles.

Tapable uses gestures

The tapable package expose many Hook classes, which can be used to create hooks for plugins.

In the compilation process of Webpack, Tapable is essentially used to implement a plug-in Plugin mechanism for publishing subscriber pattern in the compilation process.

As for the use of Plugin, I will explain its use and principle in detail in the following column. Plugin is essentially implemented based on the Tapable library.

Here, I will decouple the Webpack compilation process and take you to be familiar with the use and principle of Tapable alone, so it does not need too much pre-knowledge, please rest assured to eat boldly.

What is a Tapable

Tapable provides a publisk-subscribe API for a series of events. With Tapable we can register events and trigger registered events at different times to execute.

The Plugin mechanism in Webpack is based on this mechanism to call different plug-ins at different compilation stages and thus affect compilation results.

The official Tapable documentation provides these nine hooks:

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = require("tapable");
Copy the code

Let’s take the simplest SyncHook:

// Initialize the sync hook
const hook = new SyncHook(["arg1"."arg2"."arg3"]);

// Register events
hook.tap('flag1'.(arg1,arg2,arg3) = > {
    console.log('flag1:',arg1,arg2,arg3)
})

hook.tap('flag2'.(arg1,arg2,arg3) = > {
    console.log('flag2:',arg1,arg2,arg3)
})

// Call the event and pass the execution parameters
hook.call('19Qingfeng'.'wang'.'haoyu')

// Print the result
flag1: 19Qingfeng wang haoyu
flag2: 19Qingfeng wang haoyu
Copy the code
  • The first step is to instantiate different kinds of hooks using the new keyword.

    • New Hook takes an array of strings as a parameter. The value in the array is not important, but the number of strings in the array. We will talk about it in detail later.

    • New Hook takes a second parameter, name, which is a string. It’s not in the documentation here so you can just ignore this parameter.

  • Second, the tap function listens for the corresponding event, and accepts two parameters when registering the event:

    • The first argument is a string, which has no real meaning and is just an identifier bit. This parameter can also be an object, as I’ll show you later in the source code analysis.

    • The second argument represents the registered function that will be executed when called.

  • Finally, we pass the corresponding parameter through the call method and call the event function registered in the hook for execution.

    • At the same time, when the call method is executed, arguments passed to the call method are called as arguments to each registered event function.

Let’s start by talking about the meanings of each of the nine hooks.

Classified by synchronous/asynchronous

All registered events in Tapable can be executed synchronously or asynchronously, as the name indicates:

  • Synchronization indicates that the registered event functions are executed synchronously.

  • Asynchronous means that registered event functions are executed asynchronously.

  • The tap method is the only way to register events for synchronous hooks, and the call method triggers the execution of synchronous hooks.

  • Asynchronous hooks can be registered by tap, tapAsync, and tapPromise, while registered functions can be triggered by corresponding call, callAsync, and Promise methods.

Simultaneous asynchronous hooks can be divided into:

  • AsyncSeries: Asynchronous hook functions that can be executed in series (sequential calls).

  • AsyncParallel: Asynchronous hook functions that can be executed in parallel (called concurrently).

Categorized by execution mechanism

Tapable can categorize by asynchronous/synchronous execution as well as by execution mechanism, such as:

  • Basic Hook: A Basic type of Hook that executes only the events registered by the Hook, regardless of the return value of each event function called.

  • Waterfall: Waterfall type hook, Waterfall type hook is basically similar to the basic type hook, the only difference is that Waterfall type hook will pass the non-undefined return value of event function execution to the following event function as parameter when the registered event is executed.

  • If one of the Bail functions returns a non-undefined value, the hook execution will be interrupted immediately and the registration event will not be called.

  • Loop: Loop hooks. Loop hooks are a little more complicated. If one of the registered event functions returns a value other than undefeind when a circular hook is called via call, all registered event functions are restarted until all registered event functions return undefined.

Use 9 types of hooks

Instead of listing the use of the nine types of hooks in this article, I thought I’d show you a few short Demo cases.

But Tapable’s official documentation is too rudimentary…

SyncHook

SyncHook is the most basic SyncHook:

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

// Initialize the sync hook
const hook = new SyncHook(['arg1'.'arg2'.'arg3']);

// Register events
hook.tap('flag1'.(arg1, arg2, arg3) = > {
  console.log('flag1:', arg1, arg2, arg3);
});

hook.tap('flag2'.(arg1, arg2, arg3) = > {
  console.log('flag2:', arg1, arg2, arg3);
});

// Call the event and pass the execution parameters
hook.call('19Qingfeng'.'wang'.'haoyu');
// Print the result
flag1: 19Qingfeng wang haoyu
flag2: 19Qingfeng wang haoyu
Copy the code

SyncBailHook

If any of the event functions in SyncBailHook return a value, the subsequent event functions are immediately interrupted:

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

const hook = new SyncBailHook(['arg1'.'arg2'.'arg3']);

// Register events
hook.tap('flag1'.(arg1, arg2, arg3) = > {
  console.log('flag1:', arg1, arg2, arg3);
  // There is a return value to block the call to flag2
  return true
});

hook.tap('flag2'.(arg1, arg2, arg3) = > {
  console.log('flag2:', arg1, arg2, arg3);
});

// Call the event and pass the execution parameters
hook.call('19Qingfeng'.'wang'.'haoyu');
// Print the result
flag1: 19Qingfeng wang haoyu
Copy the code

SyncWaterfallHook

SyncWaterfallHook The waterfall hook passes the return value of the previous function to the next function as an argument:

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

// Initialize the sync hook
const hook = new SyncWaterfallHook(['arg1'.'arg2'.'arg3']);

// Register events
hook.tap('flag1'.(arg1, arg2, arg3) = > {
  console.log('flag1:', arg1, arg2, arg3);
  // There is a return value that modifies the flag2 argument
  return 'github';
});

hook.tap('flag2'.(arg1, arg2, arg3) = > {
  console.log('flag2:', arg1, arg2, arg3);
});

hook.tap('flag3'.(arg1, arg2, arg3) = > {
  console.log('flag3:', arg1, arg2, arg3);
});

// Call the event and pass the execution parameters
hook.call('19Qingfeng'.'wang'.'haoyu');
// Output the result
flag1: 19Qingfeng wang haoyu
flag2: github wang haoyu
flag3: github wang haoyu
Copy the code

It is important to note that SyncWaterfallHook only modifies the return value of the first parameter when multiple parameters exist.

SyncLoopHook

SyncLoopHook will restart execution if any of the monitored functions has a non-undefined return value:

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

let flag1 = 2;
let flag2 = 1;

// Initialize the sync hook
const hook = new SyncLoopHook(['arg1'.'arg2'.'arg3']);

// Register events
hook.tap('flag1'.(arg1, arg2, arg3) = > {
  console.log('flag1');
  if(flag1 ! = =3) {
    returnflag1++; }}); hook.tap('flag2'.(arg1, arg2, arg3) = > {
  console.log('flag2');
  if(flag2 ! = =3) {
    returnflag2++; }});// Call the event and pass the execution parameters
hook.call('19Qingfeng'.'wang'.'haoyu');
// Execution result
flag1
flag1
flag2
flag1
flag2
flag1
flag2
Copy the code

This code is actually relatively simple, but slightly convoluted.

After all, the rule is that if an event has a non-undefined return value, it should be reversed and re-executed from the beginning.

AsyncSeriesHook

AsyncSeriesHook indicates asynchronous series execution:

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

// Initialize the sync hook
const hook = new AsyncSeriesHook(['arg1'.'arg2'.'arg3']);

console.time('timer');

// Register events
hook.tapAsync('flag1'.(arg1, arg2, arg3, callback) = > {
  console.log('flag1:', arg1, arg2, arg3);
  setTimeout(() = > {
    // Call callback after 1s to indicate flag1 is complete
    callback();
  }, 1000);
});

hook.tapPromise('flag2'.(arg1, arg2, arg3) = > {
  console.log('flag2:', arg1, arg2, arg3);
  // tapPromise returns Promise
  return new Promise((resolve) = > {
    setTimeout(() = > {
      resolve();
    }, 1000);
  });
});

// Call the event and pass the execution parameters
hook.callAsync('19Qingfeng'.'wang'.'haoyu'.() = > {
  console.log('All executed done');
  console.timeEnd('timer');
});
// Print the result
flag1: 19Qingfeng wang haoyu
flag2: 19Qingfeng Wang Haoyu all executed donetimer: 2.012s
Copy the code

The code is simple, and there are two additional points I’d like to highlight:

  • TapAsync registration accepts an additional callback at the end of the argument, which indicates that the event is completed.

    If the first argument is passed as an error object, the execution of the callback will be interrupted if an error occurs.

    As in Nodejs, the following parameters represent the return value of the call, starting with the second parameter of the callback function.

  • In the same way, if the Promise returns a reject state, it does the same thing as passing an error argument to the callback, interrupting subsequent execution.

AsyncSeriesBailHook

AsyncSeriesBailHook stands for asynchronous serial bailhook:

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

// Initialize the sync hook
const hook = new AsyncSeriesBailHook(['arg1'.'arg2'.'arg3']);

console.time('timer');

// Register events
hook.tapPromise('flag1'.(arg1, arg2, arg3, callback) = > {
  console.log('flag2:', arg1, arg2, arg3);
  return new Promise((resolve, reject) = > {
    setTimeout(() = > {
      Any value in resolve indicates the presence of a return value
      // There is a return value bail
      resolve(true);
    }, 1000);
  });
});

// Flag2 will not be executed
hook.tapAsync('flag2'.(arg1, arg2, arg3,callback) = > {
  console.log('flag1:', arg1, arg2, arg3);
  setTimeout(() = > {
    callback();
  }, 1000);
});

// Call the event and pass the execution parameters
hook.callAsync('19Qingfeng'.'wang'.'haoyu'.() = > {
  console.log('All executed done');
  console.timeEnd('timer');
});
// Print the resultFlag2:19Qingfeng Wang Haoyu All executed donetimer: 1.012s
Copy the code

AsyncSeriesWaterfallHook

AsyncSeriesWaterfallHook:

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

// Initialize the sync hook
const hook = new AsyncSeriesWaterfallHook(['arg1'.'arg2'.'arg3']);

console.time('timer');

// Register events
hook.tapPromise('flag1'.(arg1, arg2, arg3) = > {
  console.log('flag2:', arg1, arg2, arg3);
  return new Promise((resolve, reject) = > {
    setTimeout(() = > {
      resolve(true);
    }, 1000);
  });
});

hook.tapAsync('flag2'.(arg1, arg2, arg3, callback) = > {
  console.log('flag1:', arg1, arg2, arg3);
  setTimeout(() = > {
    callback();
  }, 1000);
});

// Call the event and pass the execution parameters
hook.callAsync('19Qingfeng'.'wang'.'haoyu'.() = > {
  console.log('All executed done');
  console.timeEnd('timer');
});
// Output the result
flag2: 19Qingfeng wang haoyu
flag1: trueWang Haoyu all executed donetimer: 2.012s
Copy the code

AsyncParallelHook

AsyncParallelHook An asynchronous parallel hook that executes all asynchronous hooks concurrently:

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

// Initialize the sync hook
const hook = new AsyncParallelHook(['arg1'.'arg2'.'arg3']);

console.time('timer');

// Register events
hook.tapPromise('flag1'.(arg1, arg2, arg3) = > {
  console.log('flag2:', arg1, arg2, arg3);
  return new Promise((resolve, reject) = > {
    setTimeout(() = > {
      resolve(true);
    }, 1000);
  });
});

hook.tapAsync('flag2'.(arg1, arg2, arg3, callback) = > {
  console.log('flag1:', arg1, arg2, arg3);
  setTimeout(() = > {
    callback();
  }, 1000);
});

// Call the event and pass the execution parameters
hook.callAsync('19Qingfeng'.'wang'.'haoyu'.() = > {
  console.log('All executed done');
  console.timeEnd('timer');
});
// Execution result
flag2: 19Qingfeng wang haoyu
flag1: 19Qingfeng Wang Haoyu all executed donetimer: 1.010s
Copy the code

As you can see, the final callback prints a little more than 1s, so flag1 and FLAGe2 start executing in parallel, and then after 1s, the asynchronous functions end and the callback ends.

AsyncParallelBailHook

AsyncParallelBailHook Is an asynchronous parallel bailhook.

Modify the Demo a little bit and let’s look at the result:

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

// Initialize the sync hook
const hook = new AsyncParallelBailHook(['arg1'.'arg2'.'arg3']);

console.time('timer');

// Register events
hook.tapPromise('flag1'.(arg1, arg2, arg3) = > {
  return new Promise((resolve, reject) = > {
    console.log('flag1 done:', arg1, arg2, arg3);
    setTimeout(() = > {
      resolve(true);
    }, 1000);
  });
});

hook.tapAsync('flag2'.(arg1, arg2, arg3, callback) = > {
  setTimeout(() = > {
    console.log('flag2 done:', arg1, arg2, arg3);
    callback();
  }, 3000);
});

hook.callAsync('19Qingfeng'.'wang'.'haoyu'.() = > {
  console.log('All executed done');
  console.timeEnd('timer');
});

// Execution resultFlag1 done: 19Qingfeng Wang Haoyu all executed donetimer: 1.013s
flag2 done: 19Qingfeng wang haoyu

Copy the code

As you can see in flag1, resolve(true) returns a non-undefined value, and the hook stops all subsequent calls to the event function.

So first it prints:

// flag1 prints after execution
flag1 done: 19Qingfeng wang haoyu

// The whole hook is printedAll operations are completedtimer: 1.013s
Copy the code

After that, because of asynchronous parallelism, all event functions are executed in parallel at the beginning.

Since the FLAG2 event function initially calls the timer, the final timer prints after 3 seconds. In flag1, the hook is already executed because of the Bail effect.

So it will eventually print:

Flag1 done: 19Qingfeng Wang Haoyu all executed donetimer: 1.013s

// The callback callback has been executed
// But since the previous asynchronous parallel timer is not terminated, the timer will be printed after 3s
flag2 done: 19Qingfeng wang haoyu
Copy the code

Additional Hooks

The official Readme only provides the above nine hooks, with an AsyncSeriesLoopHook exposed in the source code.

Hooks are used as the name suggests, asynchronous serial loop hooks. I won’t expand on the specific usage, but if you are interested, you can try it privately.

The interceptor

All of Tapable’s hooks support injection Interception, which works much like the interceptors in Axios.

Interceptors can be used to listen on the entire Tapable publish/subscribe process and trigger the corresponding logic.

const hook = new SyncHook(['arg1'.'arg2'.'arg3']);

hook.intercept({
  // This method is called each time the tap() method of the hook instance is called to register the callback function,
  // And accept tap as an argument, can also be modified tap;
  register: (tapInfo) = > {
    console.log(`${tapInfo.name} is doing its job`);
    return tapInfo; // may return a new tapInfo object
  },
  // The interceptor is fired when the call method on the hook instance object is passed
  call: (source, target, routesList) = > {
    console.log('Starting to calculate routes');
  },
  Execute before calling each event function that is registered
  tap: (tap) = > {
    console.log(tap, 'tap');
  },
  Trigger the interceptor method before each event function in the loop type hook is called
  loop: (. args) = > {
    console.log(args, 'loop'); }});Copy the code
  • Register: The Register interceptor is triggered each time an event function is registered with the TAP, tapAsync, and tapPromise methods. The interceptor accepts the registered Tap as a parameter and can modify the registered event.

  • Call: executed by calling the call method of a Hook instance object. (Including callAsync, promise)

  • Tap: executed before each registered event function call, taking the corresponding TAP object as an argument.

  • Loop: In a loop type hook, the interceptor is executed each time the loop is restarted. The interceptor function takes the arguments passed in when the call is made.

HookMap && Context && MultiHook &&

The related module APIS for Tapable are B eFore && Stage, HookMap, Context, and HookMap.

Before && stage

Tapable supports passing in an object as the first argument when registering an event function.

We can control the timing of the registered event function execution through the stage and before properties on this object.

Before the attribute

The value of the before attribute can be passed in as an array or string of the name of the event object that was registered, which modifies the current event function to be executed before the function with the event name that was passed in.

Such as:

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

const hooks = new SyncHook();

hooks.tap(
  {
    name: 'flag1',},() = > {
    console.log('This is flag1 function.'); }); hooks.tap( {name: 'flag2'.// Flag2 will be executed before FLAG1
    before: 'flag1',},() = > {
    console.log('This is flag2 function.'); }); hooks.call();// result
This is flag2 function.
This is flag1 function.
Copy the code

Stage property

The stage attribute is of type number. The higher the number, the later the event callback is executed. Negative numbers are supported.

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

const hooks = new SyncHook();

hooks.tap(
  {
    name: 'flag1'.stage: 1,},() = > {
    console.log('This is flag1 function.'); }); hooks.tap( {name: 'flag2'.// Default stage: 0,
  },
  () = > {
    console.log('This is flag2 function.'); }); hooks.call();// result
This is flag2 function.
This is flag1 function.
Copy the code

Both before and stage can modify the execution time of event callback functions, but mixing the two attributes is not recommended. In other words, don’t appear before if you choose to use stage in your links.tap, and vice versa.

HookMap

HookMap is essentially a helper class that allows us to better manage hooks:

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

// Create a HookMap instance
const keyedHook = new HookMap((key) = > new SyncHook(['arg']));

// Create a hook with name key1 in keyedHook and register events for the hook via tap
keyedHook.for('key1').tap('Plugin 1'.(arg) = > {
  console.log('Plugin 1', arg);
});

// Create a hook with name key2 in keyedHook and register events for the hook via tap
keyedHook.for('key2').tap('Plugin 2'.(arg) = > {
  console.log('Plugin 2', arg);
});

// Create a hook with name key1 in keyedHook and register events for the hook via tap
keyedHook.for('key3').tap('Plugin 3'.(arg) = > {
  console.log('Plugin 3', arg);
});

// Get hook with name key1 from HookMap
const hook = keyedHook.get('key1');

if (hook) {
  // Trigger the Hook with the call method
  hook.call('hello');
}
Copy the code

MultiHook

MultiHook is not very common in daily applications. Its main function is to batch register event functions in multiple hooks through MultiHook.

As I mentioned briefly in the following source code analysis section, its implementation is nothing more than an additional layer of encapsulation.

Context

Context in the source code if you pass the Context argument, you get this logic:

When you use the Context Api, the console will tell you that the Api is going to be deprecated in the future, and it’s going to be deprecated in a very limited way that I won’t go through here.

Tapable source code implementation

Why do I recommend that you read the Tapable principle

If you just want to develop the Webpack Plugin, you can use it in your daily business.

There is not a lot of code about the internal principles of Tapable, so learning the principles of Tapable is the first thing that will make it easier for you to develop the Webpack Plugin.

Secondly, the internal implementation of Tapable library in my opinion is a particularly clever way to implement a publish and subscribe model, which will have a lot of knowledge: such as dynamic generation of executable code ideas, object-oriented thinking about classes and abstract class inheritance, sublimation of this point and so on…

In my opinion, the Tapable source code contains design principles and implementation procedures that are worth reading for every front-end developer.

Before diving into the source code

Before diving into the source code, LET me show you a bit of code like this:

This code creates an instance of a synchronized Hook using SyncHook, registers the two events through the tap method, and then invokes them through the call method.

Essentially this code calls hook. Call (‘arg1′,’agr2’) and Tapable dynamically compiles a function like this:

function fn(arg1, arg2) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(arg1, arg2);
    var _fn1 = _x[1];
    _fn1(arg1, arg2);
}
Copy the code
  • Get the _x property of the caller through this._x. The corresponding subscript element is then retrieved from the _x attribute.

Here _x[0] is the body of the event function for the first FLAG1 we listen for.

Similarly, _x[1] is the body of the FLAG2 function that the TAP method listens for.

A Hook object is also generated with the following properties:

const hook = {
  _args: [ 'arg1'.'arg2'].name: undefined.taps: [{type: 'sync'.fn: [Function (anonymous)], name: 'flag1' },
    { type: 'sync'.fn: [Function (anonymous)], name: 'flag2'}].interceptors: []._call: [Function: CALL_DELEGATE],
  call: [Function: anonymous],
  _callAsync: [Function: CALL_ASYNC_DELEGATE],
  callAsync: [Function: CALL_ASYNC_DELEGATE],
  _promise: [Function: PROMISE_DELEGATE],
  promise: [Function: PROMISE_DELEGATE],
  _x: [[Function (anonymous)], [Function (anonymous)] ],
  compile: [Function: COMPILE],
  tap: [Function: tap],
  tapAsync: [Function: TAP_ASYNC],
  tapPromise: [Function: TAP_PROMISE],
  constructor: [Function: SyncHook]
} 
Copy the code

All Tapable does is dynamically compile the above function body and create the Hook instance object based on the corresponding contents in the Hook.

Finally, when we Call through Call, we execute this code:

// fn generates the final fn function that needs to be executed
// hook is a hook instance object created inside tapable
hook.call = fn
hook.call(arg1, arg2)
Copy the code

The core of Tapable source code is around generating these two parts of content (a dynamically generated FN and a hook instance object that calls FN).

There are two classes in the source code to manage the contents of these two parts:

  • Hook class, responsible for creating and managing the above Hook instance object. This object is hereinafter referred to as the core Hook instance object.

  • The HookCodeFactory class is responsible for compiling the function fn that will eventually be called by hook based on the content. This function is hereinafter referred to as the resulting execution function.

Deep into Tapable source code

The clone code, which you can see here, is strongly recommended.

Entrance to the file

Let’s dive into the Tapable source code to talk about its implementation.

First, it exports a lot of hook functions in its entry file:

"use strict";

exports.__esModule = true;
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");
Copy the code

Let’s start with the basics of SyncHook and try Tapable step by step.

I don’t follow the source code script in this article, because I personally find it difficult to digest and obscure for most people.

Here I will take you step by step to implement Tapable, in the implementation process I will try to restore according to the source code one-to-one. But I’m also going to weed out some useless code that might interfere with your thinking, like the Context argument we mentioned above.

Starting from the SyncHook

Let’s start with the simplest SyncHook and implement the basic SyncHook process step by step.

To do a good job, he must sharpen his tools.

In the entry file of the source code, we can see that different hooks are stored in different files. Let’s create the basic directory first.

  • Here we create an index.js as the project entry file

  • A syncook.js file is also created to hold the synchronization basic hook logic.

  • Also create hook. js, which is the parent of all types of hooks from which all hooks are derived.

  • Also create a hookcodeFactory.js file to generate the final functions that need to be executed.

// What the import file does is very simple
exports.SyncHook = require('./SyncHook');
Copy the code
// Basic SyncHook file
function SyncHook () {}module.exports = SyncHook
Copy the code

Hookcodefactory.js and hookcodefactory.js we don’t need to fill any logic for now.

Implement SyncHook. Js

Let’s start by filling in the basic SyncHook logic:

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

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

function SyncHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	hook.constructor = SyncHook;
	hook.tapAsync = TAP_ASYNC;
	hook.tapPromise = TAP_PROMISE;
        // COMPILE method you can ignore it for the moment and I'm not implementing COMPILE method here either
	hook.compile = COMPILE;
	return hook;
}

SyncHook.prototype = null;

module.exports = SyncHook;
Copy the code

Here we supplement the basic logic of the SyncHook function, which we use explicitly to instantiate Hook objects through New SyncHook.

So here when we do new SyncHook

  • First, the basic Hook instance object is created through New Hook(args, name).

  • There are no tapAsync and tapPromise methods in synchronized hook, so corresponding error functions are assigned to the two methods of hook object respectively.

  • Return the Hook instance object and set the SyncHook prototype to NULL.

When we pass new SyncHook([1,2]), the corresponding hook instance object is returned.

It might have been more intuitive to write using ES6 classes, but I still used traditional constructors to restore the source code.

Careful students might notice two things SyncHook doesn’t do:

  • Hook the parent class.

  • Hook.compile = COMPILE method in COMPILE method.

Let’s take a look at the Hook parent class object first. All types of hooks are inherited from this Hook class, and the instance of this basic Hook class is also the so-called core Hook instance object.

Hook. Js

Initialize the
class Hook {
  constructor(args = [], name = undefined) {
    // Save the arguments passed when the Hook is initialized
    this._args = args;
    // The name argument is useless
    this.name = name;
    // Save the content registered via TAP
    this.taps = [];
    We'll ignore interceptors for now
    this.interceptors = [];
    // hook. Call calls the method
    this._call = CALL_DELEGATE;
    this.call = CALL_DELEGATE;
    // _x holds all functions registered by tap in hook
    this._x = undefined;

    // Dynamic compilation method
    this.compile = this.compile;
    // The relevant registration method
    this.tap = this.tap;

    // Synchook-independent code
    // this._callAsync = CALL_ASYNC_DELEGATE;
    // this.callAsync = CALL_ASYNC_DELEGATE;
    // this._promise = PROMISE_DELEGATE;
    // this.promise = PROMISE_DELEGATE;
    // this.tapAsync = this.tapAsync;
    // this.tapPromise = this.tapPromise;
  }

  compile(options) {
    throw new Error('Abstract: should be overridden'); }}module.exports = Hook;
Copy the code

Let’s start by filling in the base Hook. Js code. I’ve commented out any code that isn’t relevant to SyncHook.

You can see that we initialized a set of properties in the constructor of the Hook.

The this.tap registration method and the CALL_DELEGATE method will be implemented step by step.

Here you need to figure out what properties are stored inside Tapable in new SyncHook(Args).

The so-called compile method is the entry method to compile the execution function we eventually generate, and we can see that the compile method is not implemented in the Hook class.

This is because different types of hooks end up compiling different forms of execution functions, so we hand the compile method to subclasses to implement in an abstract way.

Implement the TAP registration method

Let’s implement the tap() registration method in Hook. We usually register events with SyncHook instance objects in this way:

hook.tap(name, (arg) = > {
    // dosomething
})
Copy the code

Because the tap() method registration logic is consistent across different kinds of hooks, adding the listening name and the corresponding execution function fn to this.taps by changing the method, it is best to implement it in the parent class.

// Hook.js

class Hook {...tap(options, fn) {
    // There is an extra layer of wrapping here because this._tap is a generic method
    // We are using sync here, so the first parameter indicates that the type is passed in sync
    Sync, promise, promise, sync, promise, sync, promise, sync, promise
    this._tap('sync', options, fn);
  }

  / * * * *@param {*} Type Indicates the registration type promise, async, sync *@param {*} The first argument object * passed when options is registered@param {*} Fn registers when passing in the listening event function */
  _tap(type, options, fn) {
    if (typeof options === 'string') {
      options = {
        name: options.trim(),
      };
    } else if (typeofoptions ! = ='object' || options === null) {
      // If it is not an object or null is passed
      throw new Error('Invalid tap options');
    }
    // The only remaining options are object
    if (typeofoptions.name ! = ='string' || options.name === ' ') {
      // If the options.name passed in is not a string or an empty string
      throw new Error('Missing name for tap');
    }
    // merge parameters {type, fn, name:' XXX '}
    options = Object.assign({ type, fn }, options);
    // Insert the merged parameters
    this._insert(options)
  }
  
  _insert(item) {
    // this._resetCompilation(); _resetCompilation is going to add some actual logic to this later
    this.taps.push(item)
  }
}
Copy the code

Here we add the logic to the tap(name,args) method, which is essentially called into the tap() method above when the hook.tap() method is called.

We can see that the prototype method tap on the Hook class takes a second argument that is not only a string but also an object. Such as:

hook.tap({
    name: 'flag1'
}, (arg) = > {
    // dosomething
})
Copy the code

The tap() method is used to pass string/object as the first parameter. The tap() method is used to pass string/object as the first parameter. The tap() method is used to pass object as the first parameter, and the tap() method is used to pass before and stage attributes. I’ll fill you in on this logic later.

You can see that when we call hook. Tap to register the event, we end up inserting an object {type:’sync’,name:string, fn: Function} in this. Taps.

Implement the call call method

As mentioned at the beginning of the source code analysis, Tapable eventually compiles a corresponding function when we call the call() method – the resulting execution function.

The inner core of a real call method is the dynamic generation of the resulting execution function when a hook. Call is called, and the resulting execution function is called from a hook instance object.

const CALL_DELEGATE = function(. args) {
	this.call = this._createCall("sync");
	return this.call(... args); };class Hook {
    	constructor(args = [], name = undefined) {
                // ...
		this._call = CALL_DELEGATE;
		this.call = CALL_DELEGATE;
                // ...}...// Compile the final generated method to execute the function
        Compile is an abstract method that needs to be implemented in a subclass that inherits Hook
        _createCall(type) {
          return this.compile({
            taps: this.taps,
            // interceptors: this. Interceptors, ignore the interceptors first
            args: this._args
            type: type, }); }}Copy the code

As you can see inside Tapable, the this.call method initially points to the CALL_DELEGATE method.

The CALL_DELEGATE method is internally compiled through this._createcall (“sync”) to generate the resulting execution function.

To assign the generated function to this.call, call this.call(… Args) calls the resulting execution function.

The CALL_DELEGATE here is only executed when this.call is called; in other words, the hook. Call method is compiled once each time it is called – from the event function registered inside the hook, called the resulting execution function.

That is, the initial hook.call method inside a hook instance refers only to the CALL_DELEGATE method. The CALL_DELEGATE method is only executed when hook.call() is called to assign the hook. Call to the resulting execution function after compilation, which you can think of as lazy (dynamic) compilation.

Enclosing _resetCompilation method

I commented out the this._resetcompilation () method on the _insert method above, so I’ll walk you through what that method does step by step and fill in the logic when appropriate.

First, let’s recall what we said above:

const { SyncHook } = require('tapable')

const hooks = new SyncHook(['arg1'.'arg2'])

hooks.tap('flag1'.() = > {
    console.log(1)
})

hooks.tap('flag'.() = > {
    console.log(2)
})

hooks.call('arg1'.'arg2')
Copy the code

Call (‘arg1′,’arg2’) is the same as calling this.call(‘arg1′,’arg2’) in the Demo above.

This. Call now calls the method

  • First comes a call to CALL_DELEGATE, this._createCall dynamically generates the resulting execution function, which looks like this:
function fn(arg1, arg2) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(arg1, arg2);
    var _fn1 = _x[1];
    _fn1(arg1, arg2);
}
Copy the code

I’ll go into the compilation details later, but HERE I think you get the idea of the whole process.

  • The this._createCall(‘sync’) method returns the resulting function, which is reassigned to this. Call this.

Lazy compilation is exactly what it means, dynamically compiling a function that needs to be executed each time a hook. Call is called.

_x, which is called hook._x, contains a list of events registered by hook. Tap.

Now let’s try to modify the Demo a little bit:

const { SyncHook } = require('tapable')

const hooks = new SyncHook(['arg1'.'arg2'])

hooks.tap('flag1'.() = > {
    console.log(1)
})

hooks.tap('flag'.() = > {
    console.log(2)
})

hooks.call('arg1'.'arg2')

// Add a tap event function again
hooks.tap('flag3'.() = > {
  console.log(3);
});

// call again at the same time
hooks.call('arg1'.'arg2');
Copy the code

In the Demo above, when the hooks. Call method was first called, it was clear that Tapable internally compiles the resulting execution function and assigns it to the hooks.

Now I added a flag3 event function. What happens when I call hooks.call again?

Yes, at this point in the process, hooks.call still prints 1 and 2, and does not fire the Flag3 event function.

This is because the hooks. Call method is already compiled as the result of the first call and overwrites the original compilation method CALL_DELEGATE assignment to hook. Call.

The this._resetCompilation method is designed to solve this problem.

class Hook {...// Each tap calls _resetCompilation to reassign this.call
      _resetCompilation() {
        this.call = this._call;
      }

      _insert(item) {
        this._resetCompilation();
        this.taps.push(item); }}Copy the code

The _insert method is triggered each time we register the method with hooks. Tap, so we reset this. Call to the compile method CALL_DELEGATE each time in the _INSERT method.

The this.call method is reset each time the tap method registration function is called.

This. _call is initialized in the constructor of the Hook, which is the CALL_DELEGATE.

In-depth HookCodeFactory. Js

Above we through the Hook. Js file to achieve the basic Hook instance of the attribute initialization and method, through the initialization of Hook.

Let’s dive into hookCodeFactory.js to explore how Tapable compiles and generates the resulting executor.

Hook. Js the Compile method

In the parent class of Hook. Js, we do not implement the compile method. We said that each compile method results in a different type of Hook compilation function.

So let’s go back to syncook.js and look at the compile method in SyncHook:

// SyncHook.js
const Hook = require('./Hook');
const HookCodeFactory = require('./HookCodeFactory');

class SyncHookCodeFactory extends HookCodeFactory {
  // You can ignore the content method for now
  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');
};

This.call () -> CALL_DELEGATE() -> this._createcall () -> this.compile() -> COMPILE() *@param {*} options
 * @returns* /
function COMPILE(options) {
  factory.setup(this, options);
  return factory.create(options);
}

function SyncHook(args = [], name = undefined) {
  const hook = new Hook(args);
  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

In syncook.js I added the legacy hook.compile method.

Don’t worry, let me break down the supplement for you a bit:

  • The hook.compile method is called when the hook.call is called, and the accepted options argument has the following properties:

    • Taps represents an array of taps, [{type, fn, name:’ XXX ‘}…] .

    • Interceptors, we’ll ignore interceptors here.

    • Args is the argument we passed in when we new hook, it’s an array.

    • Type indicates the type of hook, in this case ‘sync’.

{
      taps: this.taps,
      interceptors: this.interceptors,
      args: this._args,
      type: type,
    }
Copy the code
  • The HookCodeFactory class is the method class that compiles and generates the resulting execution function, which is the base class. Tapable pulls the same logic out of the final methods generated by different kinds of Hook compilations to this class.

  • SyncHookCodeFactory SyncHookCodeFactory is a subclass of HookCodeFactory, which is used to store different content method implementations in different types of hooks.

You can ignore what the Content method does for now.

Here’s about factory.setup(this, options) in the COMPILE method; The first argument here, this, is actually the Hook instance object we created with new Hook().

  • The SyncHookCodeFactory instance inside the COMPILE method called Factory initializes factory.setup(this, Options) and create the resulting execution function with factory.create(options) and return it.

In fact, Tapable code is very clear, different classes are responsible for different logic processing.

The common logic is implemented in the base class, while the differentiated logic is implemented in different subclasses based on abstract classes.

Hookcodefactory.js basic skeleton
class HookCodeFactory {
  constructor(config) {
    this.config = config;
    this.options = undefined;
    this._args = undefined;
  }

  // Initialize parameters
  setup(instance, options) {}

  // Compile the final function to be generated
  create(options){}}module.exports = HookCodeFactory;
Copy the code

As mentioned above, we called the setup and create methods on the HookCodeFactory instance factory in hook.compile.

The setup method

The setup method is implemented very simply to initialize a collection of current events.

class HookCodeFactory {...// Initialize parameters
      setup(instance, options) {
        instance._x = options.taps.map(i= > i.fn)
      }
    ...
}
Copy the code

Two arguments accepted in the setup function:

  • The first argument is the this object in the COMPILE method, which is the Hook instance object we generate with new Hook.

  • The second argument is the options object passed by _createCall on the Hook class when the COMPILE method is called.

{
			taps: this.taps,
			interceptors: this.interceptors,
			args: this._args,
			type: type
		}
Copy the code

If you forget what this parameter means, you can check the Hook. Js Compile method here.

Every time we call hook. Call, we will first setup the hook instance object by assigning _x to all event functions registered by tap [fn1,fn2… .

The create method

It is through the Create method on the HookCodeFactory class in Tapable that the core logic of compiling the function is implemented.

The create method on the HookCodeFactory class compiles this function:

function fn(arg1, arg2) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(arg1, arg2);
    var _fn1 = _x[1];
    _fn1(arg1, arg2);
}
Copy the code

Let’s implement the create method step by step.

class HookCodeFactory {
  constructor(config) {
    this.config = config;
    this.options = undefined;
    this._args = undefined;
  }

  // Initialize parameters
  setup(instance, options) {
    instance._x = options.taps.map((i) = > i.fn);
  }

  // Compile the final function to be generated
  create(options) {
    this.init(options);
    // Finally compile the generated method fn
    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;
      // Other types are not considered
      default:
        break;
    }
    this.deinit();
    return fn;
  }

  / * * *@param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options
   */
  init(options) {
    this.options = options;
    // Save the parameters used to initialize the Hook
    this._args = options.args.slice();
  }

  deinit() {
    this.options = undefined;
    this._args = undefined; }}module.exports = HookCodeFactory;

Copy the code

Here, we create a create method on the HookCodeFactory class. This method has three macro aspects:

  • This.init (), which first initializes the associated properties at compile time.

  • The switch method matches different types of hooks for compilation, so you can ignore the specific compilation logic for now.

  • This.deinit (), when the result has been compiled and assigned to fn, we need to unassign the related parameters.

In the switch statement, we use new Function to dynamically build the final Function to be executed. Next, I implement the logic in the Switch statement step by step.

this.args() && this.header()

In the create method we can see that the final Function is generated by new Function().

The two methods this.args() and this.header() are the same logic for different kinds of hooks.

Since the function arguments and the top content of the function are similar, this is implemented directly in the HookCodeFactory parent class.

class HookCodeFactory {...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 method is very simple. It simply converts the this._args array stored in the class to a string that is passed to the corresponding new Function statement.

The before and after parameters don’t exist in SyncHook, so you can ignore them for now. For example, an asynchronous hook receives an additional callback for each event function we call, which is passed in through After.

Let’s first look at the header method in the source code:

  header() {
    let code = ' ';
    NeedContext () is false. The context API is about to be deprecated
    if (this.needContext()) {
      code += 'var _context = {}; \n';
    } else {
      code += 'var _context; \n';
    }
    code += 'var _x = this._x; \n';
    // There is no interceptor
    if (this.options.interceptors.length > 0) {
      code += 'var _taps = this.taps; \n';
      code += 'var _interceptors = this.interceptors; \n';
    }
    return code;
  }
Copy the code

This is a little comment I made for the header method in the source code, the part about interceptors and needContext, so let’s just skip that logic so we don’t get confused.

class HookCodeFactory {
  // ...
  header() {
    let code = ' ';
    code += 'var _context; \n';
    code += 'var _x = this._x; \n';
    return code;
  }
  // ...
}
Copy the code

The this.header method Tapable generates a string that looks like this:

var _context;
var _x = this._x
Copy the code

At this point, we’re done with the arguments to the new Function and the header part of the Function.

About to generate compiled eventually need to perform Function is essentially by enclosing the header method and enclosing contentWithInterceptors method returns the string concatenation is called Function content, in call new Function constructor objects.

this.contentWithInterceptors

Enclosing contentWithInterceptors people as the name implies, generating function content, and the content of interceptor. Let’s ignore the interceptor part and look at this simplified method:

class HookCodeFactory {

  create(options) {
    this.init(options);
    // Finally compile the generated method fn
    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;
      // Other types are not considered
      default:
        break;
    }
    this.deinit();
    return fn;
  }
  
  
    // ...
    contentWithInterceptors(options) {
      // If there are interceptors
        if (this.options.interceptors.length > 0) {
            // ...
        }else {
            return this.content(options); }}// ...
}
Copy the code

Here are a few things to watch out for:

  • Call this. ContentWithInterceptors function passes the object has a huge number of attributes, here we just need to use is onError and onDone these two methods.

  • The contentWithInterceptors method first checks if there is an interceptor, and then calls this.content(options) to generate the body and return it if there is no interceptor.

In SyncHookCodeFactory, there is an instance method called Content.

// SyncHook.js.const HookCodeFactory = require('./HookCodeFactory');


class SyncHookCodeFactory extends HookCodeFactory {
  // You can ignore the content method for now
  content({ onError, onDone, rethrowIfPossible }) {
    return this.callTapsSeries({
      onError: (i, err) = >onError(err), onDone, rethrowIfPossible, }); }}...Copy the code

As we have said before, because the function code generated by different Hook types is inconsistent, Tapable will be stored in the parent class HookCodeFactory based on the same compilation logic, and each Hook will inherit the common logic of the parent class, while implementing differentiated logic in their own subclasses.

The SyncHookCodeFactory class here is a SyncHook’s own subclass compiled object.

When the hook. Call method is called, content on the SyncHookCodeFactory subclass is eventually called to generate the corresponding function content.

The content method of SyncHookCodeFactory in Synchok. js calls the this.callTapsSeries method of HookCodeFactory.

Doesn’t that feel very convoluted, ha ha. But why?

Tapable uses this design approach to organize code to better decouple modules.

This.TapsSeries compile-generated executable function

There are a lot of boundary cases and other logic handling in the this.TapsSeries source code. Here I’ve streamlined the source code and removed the logic solely associated with SyncHook.

class HookCodeFactory {...contentWithInterceptors(options) {
    // If there are interceptors
    if (this.options.interceptors.length > 0) {
      // ...
    } else {
      return this.content(options); }}// Generate the overall function content based on this._x
  callTapsSeries({ onDone }) {
    let code = ' ';
    let current = onDone;
    // Unregistered events are returned directly
    if (this.options.taps.length === 0) return onDone();
    // The taps registry functions are compiled to generate functions that need to be executed
    for (let i = this.options.taps.length - 1; i >= 0; i--) {
      const done = current;
      // Create the corresponding function calls one by one
      const content = this.callTap(i, {
        onDone: done,
      });
      current = () = > content;
    }
    code += current();
    return code;
  }

  Fn1 = this._x[0]; fn1 = this._x[0]; fn1 = this._x[0]; fn1(... args)
  callTap(tapIndex, { onDone }) {
    let code = ' ';
    // No matter what type, the content should be obtained by subscript first
    Var _fn[1] = this._x[1]
    code += `var _fn${tapIndex} = The ${this.getTapFn(tapIndex)}; \n`;
    // Different types call differently
    // Generate call code fn1(arg1,arg2...)
    const tap = this.options.taps[tapIndex];
    switch (tap.type) {
      case 'sync':
        code += `_fn${tapIndex}(The ${this.args()}); \n`;
        break;
      // Other types are not considered
      default:
        break;
    }
    if (onDone) {
      code += onDone();
    }
    return code;
  }
  
  This. _x[index] = this._x[index]
  getTapFn(idx) {
    return `_x[${idx}] `; }... }Copy the code
  • The callTapsSeries method iterates through all registered TAPS to compile corresponding functions that ultimately need to be executed.

  • CallTap generates the corresponding function call statement based on the type of a single TAP.

Essentially what callTapsSeries and callTap do is very simple: compile the corresponding function content based on the type of Tap and save this._x.

Verify SyncHook. Js

At this point, the SyncHook implementation is complete. We basically implemented SyncHook exactly like Tapable.

Let’s verify our own SyncHook:

I created a new synchook.js under Tapable /demo.

const { SyncHook } = require('.. /index');

const hooks = new SyncHook(['arg1'.'arg2']);

hooks.tap('1'.(arg1, arg2) = > {
  console.log('hello', arg1, arg2);
});

hooks.tap('2'.(arg1, arg2) = > {
  console.log('hello2', arg1, arg2);
});

hooks.call('wang'.'haoyu');

hooks.tap('3'.(arg1, arg2) = > {
  console.log('hello3', arg1, arg2);
});
console.log('-- -- -- -- -- -);
hooks.call('19Qingfeng'.'haoyu');
Copy the code

Execute this code and take a look at the output:

It worked out exactly as we expected, didn’t it? We’re done!

At the end of the source code analysis

If you read the above, I’m sure you’ve all figured out the basic workflow in Tapable with a SyncHook.

Dynamically compiles the stack diagram of the function calls that ultimately need to be executed when hook.call() is called.

In essence, Tapable is to store the corresponding listening properties and methods through the Hook class, and dynamically compile a Function generated by HookCodeFactory when the event is triggered by the call method, so as to achieve the corresponding effect.

Read about the source code is really obscure for most people, so I really appreciate and admire every friend who can see here.

In fact, Tapable was originally intended to do a source code interpretation implementation for the whole process synchronization, asynchronous, interceptor and HookMap process, and the end of SyncHook has been written more than 1W words.

The core design flow of Tapable can already be seen in SyncHook. I will also stop at the SyncHook implementation here, and I will add additional source code in the column if you are interested.

Talk a little about Tapable and Webpack

There are two core object Compiler and Compilation in Webapck Compilation stage.

For the basic process of Webpack compilation you can check out my article Webapck5 core packaging principle full process analysis.

When initializing Compiler and Compilation objects, Webpack creates a series of corresponding hooks as properties stored in their instance objects.

In the development of Webapck Plugin, it is based on this series of hooks that corresponding events are released at different times. Events are executed to affect the final compilation result.

I will explain Webpack Plugin in detail in the following column. The reason why I expand Tapable is to pave the way for the pre-knowledge of Webpack Plugin.

At the end

I hope this article about Tapable can help you. If there are any shortcomings in this article, please comment in the comments section

I’ll be updating my column with more Tapable source code insights and an explanation of how Webpack works.

If interested in the principle of Webpack partners can pay attention to my column from the principle of playing Webpack column.