This article is the first tapable project source code reading, the purpose of the article is to roughly comb through the tapable source code structure, for the next in-depth details to pave the truth, lay a good foundation.

The article assumes that the reader has a basic understanding of the use of Tapable, which is documented here

The overall perception

Without further ado, let’s go directly to the project structure diagram:

-tapable |-lib |-index.js |-Hook.js |-SyncHook.js |-SyncBailHook.js |-SyncLoopHook.js |-SyncWaterfallHook.js |-AsyncParallelBailHook.js |-AsyncParallelHook.js |-AsyncSeriesBailHook.js |-AsyncSeriesHook.js |-AsyncSeriesLoopHook.js  |-AsyncSeriesWaterfallHook.js |-HookCodeFactory.js |-HookMap.js |-MultiHook.js |-util-browser.js |-package.json | - __tests__ | - test casesCopy the code

The structure of the project is very simple, and you can see that all the hook types supported by Tapable and the two helper types exist as a single file.

Use the main field of package.json to find the project entry index.js

// index.js
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
// ...
Copy the code

The entry content is simple, just bringing in and exporting all 10 hook types supported by Tapable, as well as two helper classes.

Probe into the instance method of Hook

One thing that can be revealed ahead of time is that the 10 hook types exported by Tapable are basically the same in the process of instantiation, so next, we will choose the simplest SyncHook type to explore the general structure of the project.

Before looking at the code, let’s ask ourselves a question: What do we want to see in SyncHook code?

Based on the use of SyncHook, I think there are the following:

  1. Plug-in registeredtapmethods
  2. Execute all registered plug-inscallmethods
  3. Hook interceptor registrationinterceptmethods

With anticipation, we enter syncook.js

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

Using instance inheritance, the SyncHook subclass inherits the Hook parent and points constructor to itself, overwriting the tapAsync, tapPromise, and compile methods. For hooks of type SyncHook, both tapAsync and tapPromise method calls throw errors directly. The compile method, we don’t know what it does yet.

The tap, call, and Intercept methods we were expecting didn’t show up, so we went to the base class Hook.

The Hook class structure is as follows:

class Hook {
  constructor(args = [], name = undefined) {
    // ...
    this.taps = [];
    this.interceptors = [];
    this.call = CALL_DELEGATE;
    this.callAsync = CALL_ASYNC_DELEGATE;
    this.promise = PROMISE_DELEGATE;

    this.compile = this.compile;
    this.tap = this.tap;
    this.tapAsync = this.tapAsync;
    this.tapPromise = this.tapPromise;
  }

  intercept() { / * * / }
  tap() { / * * / }
  tapAsync() { / * * / }
  tapPromise() { / * * /}}Copy the code

Sure enough, all of the methods that we expect are defined here, so let’s look at them one by one.

Plug-in registration method: TAP

Let’s start with the TAP method for plug-in registration (note that TAP is the SyncHook class registration method, and there are other registration methods for other types of hooks).

class Hook {
  // ...
  tap(options, fn) {
    this._tap("sync", options, fn);
  }
  _tap(type, options, fn) {
    // omit the standardized options code, which contains type and fn information
    this._insert(options);
  }

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

  tapPromise(options, fn) {
    this._tap("promise", options, fn);
  }
  _insert(item) {
    // Omit the logical code to find the insertion location
    this.taps[i] = item; }}Copy the code

As can be seen from this, the essence of a hook instance’s plug-in registration is to standardize the parameters we pass in and push them into the TAPS array of the instance. Also, TAPS is an ordered list, presumably related to the order in which plug-ins will be executed in the future (the sorting criteria will be explained in more detail in a future article).

That’s how SyncHook registers plugins, but what about other types of plugins?

In Tapable, plug-in registration methods are divided into three categories (some hook types support multiple registration methods), sync, Async and Promise. The corresponding plug-in registration methods are TAP,tapAsync and tapPromise respectively, and the three registration methods are exactly the same. Both call this._tap to insert the plug-in configuration into the TAPS array.

Hook interceptor method: Intercept

class Hook {
  // ...
  intercept(interceptor) {
    this._resetCompilation();
    this.interceptors.push(Object.assign({}, interceptor));
    if (interceptor.register) {
      // The middleware register function is executed on all ** already ** registered plug-ins
      for (let i = 0; i < this.taps.length; i++) {
        this.taps[i] = interceptor.register(this.taps[i]); }}}}Copy the code

The plug-in registration code is also simple, essentially pushing the parameters received by the Intercept method directly into the instance property Interceptors array. At the same time, the registered plug-in is traversed, taking the plug-in’s configuration as an argument, and the register method of the current interceptor is executed. (Interceptor configuration has many other methods, such as call, tap, loop, etc., which will be executed when appropriate)

Call method for plug-in list: call

First of all, notice that call is a SyncHook plug-in invocation method, and there are other invocation methods (callAsync, promise).

Plug-in invocation methods are much more complex than plug-in registration and interceptor registration.

Back to the Hook. Js:

const CALL_DELEGATE = function(. args) {
  this.call = this._createCall("sync");
  return this.call(... args); };class Hook {
  constructor(args = [], name = undefined) {
    / /... Omit other code
    this.call = CALL_DELEGATE;
  }

  // Generate the corresponding plug-in execution based on the provided type
  _createCall(type) {
    return this.compile({
      // ...
    });
  }
  
  compile(options) {
    throw new Error("Abstract: should be overridden"); }}Copy the code

The call method execution is delegated to the CALL_DELEGATE function. The call method that actually executes the logic is generated by this._createCall, which returns the result of this.pile execution directly, so it can be summarized as, The logic called by the plug-in is generated by the compile method of the hook instance. The compile method here is an abstract method that needs to be implemented in a concrete hook.

We’re using SyncHook as an example, so go to synchook. js and find the compile method:

const factory = new SyncHookCodeFactory();

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

function SyncHook(args = [], name = undefined) {
  const hook = new Hook(args, name);
  // ...
  The compile method generates the call/callAsync/ Promise method that calls the hook plug-in
  hook.compile = COMPILE;
  return hook;
}
Copy the code
class SyncHookCodeFactory extends HookCodeFactory {
  content({ onError, onDone, rethrowIfPossible }) {
    return this.callTapsSeries({
      onError: (i, err) = >onError(err), onDone, rethrowIfPossible }); }}Copy the code

The compile method here is also generated by the Create method of the SyncHookCodeFactory instance, which inherits from HookCodeFactory and defines the Content method. If not found, continue to HookCodeFactory:

class HookCodeFactory {
  // ...
  Compile function, which returns hook calls such as synchook's call method
  create(options) {
    // ...
    let fn;
    switch (this.options.type) {
      case "sync":
        fn = new Function(
          this.args(),
          '"use strict"; \n' +
            this.header() + // Parameter definition
            this.contentWithInterceptors({
              // ...}));break;
      case "async":
        // ...
      case "promise":
        // ...
    }
    // ...
    return fn;
  }

  setup(instance, options) {
    instance._x = options.taps.map(t= > t.fn);
  }
  
  contentWithInterceptors(options) {
    if (this.options.interceptors.length > 0) {
      // ...
      let code = "";
      for (let i = 0; i < this.options.interceptors.length; i++) {
        const interceptor = this.options.interceptors[i];
        if (interceptor.call) {
          // Execute all middleware call functions
          code += `The ${this.getInterceptor(i)}.call(The ${this.args({
            before: interceptor.context ? "_context" : undefined
          })}); \n`;
        }
      }

      code += this.content(
        Object.assign(options, { / * * /}));return code;
    } else {
      return this.content(options); }}Copy the code

As mentioned earlier, the create method of the code factory class generates the compile method of the hook instance (compile executes the call method that generates the plug-in call). Look at the create method code. It uses the Function constructor to instantiate a Function based on the incoming hook type and returns the compile method of the hook instance.

Further see the instantiation of the function, main logic are enclosing contentWithInterceptors inside, the method to check if there is a blocker, if there are interceptors, before adding plugin code is executed, plus the first interceptor call method execution logic. The plugin logic code is added by this.content, which is defined in a factory type of a specific type, which for the SyncHook class is the SyncHookCodeFactory factory class.

It’s a little convoluted, but let’s try to get it straight:

SyncHook hooks are instantiated with no method for scheduling the plugin logic. There is a call, but this is a dummy function. When this function is executed, compile is called to compile the call function, which is then executed. (what? What does the _createCall method in hook. js do? If you tried to delete _createCall and call this.pile, it would be fine, but three proxy functions with three duplicate code? So when it comes to understanding the logic of the source code, it’s easier to peel off this layer.)

Compile is essentially a function that generates a scheduler plug-in, and compile directly returns the result of the create execution, so the create method is essentially a function that generates a scheduler plug-in.

The create method, defined on the base class of the factory type, uses the Function constructor to generate the logic for the plug-in scheduling method line by line. The main scheduling logic is generated by the factory class method contentWithInterceptors

ContentWithInterceptors uses the factory instance method Content to generate code for scheduling the plug-in logic. It also needs to handle interceptors. Before adding the plug-in logic, it iterates through all interceptors and adds the execution logic for the Interceptor.call method.

Reverse the logic (where Factory is an instance of the factory class) :

Factory. The content (generated plug-in scheduling subject code strings) – > factory. ContentWithInterceptors (before scheduling code plug-ins add interceptors execute code strings) – > Factory.create (adds function variable definitions and instantiates the function) -> hook.compile(sets up the factory configuration, calls create to generate and return the scheduler function) -> hook.call

The above is the initial perception of the overall structure of the tapable project. Please stay tuned for details.