Recently, I wrote an article on the fundamentals of WebPack and the use of AST. I was going to write about the principles of WebPack Plugin, but I found that WebPack Plugin is highly dependent on tapable. Looking directly at the Webpack Plugin without knowing tapable is always a bit confusing. So I took a look at the tapable documentation and source code, and found that this library is very interesting and is an enhanced publish and subscribe model. The publish-subscribe model is all too common in the source world, and we’ve seen it in multiple library sources:

  1. reduxthesubscribeanddispatch
  2. Node.jstheEventEmitter
  3. redux-sagathetakeandput

Tapable has no specific business logic. It is a tool library specially used to implement event subscription or what tapable calls hook(hook). Its basic principle is still publish and subscribe mode. But it implements multiple forms of publish and subscribe and includes multiple forms of process control.

Tapable exposes multiple apis and provides a variety of flow control methods. Even using tapable is quite complicated, so I want to write about its principle in two articles:

  1. Take a look at the usage first and experience his various flow control methods
  2. See how the source code is implemented through usage

This article is about the usage of the article, know his usage, we later if you have their own implementation of hook or event monitoring needs, can directly take over the use, very powerful!

The examples of this article have all been uploaded to GitHub, you can take them down for reference:Github.com/dennis-jian…

What is tapable

Tapable is the core module of Webpack and maintained by Webpack team. It is the basic implementation method of WebPack Plugin. Its main function is to provide users with a powerful hook mechanism, webpack Plugin is based on hook.

The main API

Here are the main apis listed in the official documentation, all with names that end in Hook:

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

The name of the API explains what it does. Note the keywords: Sync, Async, Bail, Waterfall, Loop, Parallel, Series. Here’s how to explain these keywords:

Sync: This is a synchronized hook

Async: This is an asynchronous hook

When a hook registers multiple callback methods and any one of them returns a non-undefined value, subsequent callback methods will no longer be executed, thus acting as a “fuse”.

Waterfall: Waterfall means Waterfall in English. In the programming world, it means to execute various tasks in sequence. The effect here is that when a hook registers multiple callback methods, the next callback will be executed only after the previous callback is finished, and the execution result of the previous callback will be passed as a parameter to the next callback function.

When a hook registers a callback method, the callback is repeated if the method returns true. The next callback is executed only when the callback returns undefined.

Parallel: Parallel means Parallel, somewhat like promise. all, when a hook registers multiple callback methods that start executing in Parallel at the same time.

Series: When a hook registers multiple callback methods, the first one is executed before the next one is executed.

The concepts of Parallel and Series only exist in asynchronous hooks, because synchronous hooks are all serial.

Let’s take a look at the use and effects of each API.

Synchronous API

Here are the synchronization apis:

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
 } = require("tapable");
Copy the code

As mentioned, synchronization apis are all serial, so the difference is in flow control.

SyncHook

SyncHook is the most basic hook. It is similar to the publish-subscribe model we often use. Note that all the hooks exported by Tapable are classes.

const hook = new SyncHook(["arg1"."arg2"."arg3"]);
Copy the code

Since SyncHook is a class, we use new to generate an instance. The constructor takes an array [“arg1”, “arg2”, “arg3”] that has three parameters, meaning that the generated instance receives three parameters when it registers the callback. Instance hook has two main instance methods:

  1. tap: is the method to register event callbacks.
  2. call: is the method that triggers the event and executes the callback.

Let’s expand on the official documentation of small car acceleration examples to illustrate the specific usage:

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

// Instantiate an accelerated hook
const accelerate = new SyncHook(["newSpeed"]);

// Register the first callback and record the current speed as it accelerates
accelerate.tap("LoggerPlugin".(newSpeed) = >
  console.log("LoggerPlugin".` accelerated to${newSpeed}`));// Register a callback to check for overspeed
accelerate.tap("OverspeedPlugin".(newSpeed) = > {
  if (newSpeed > 120) {
    console.log("OverspeedPlugin"."You are speeding!!"); }});// Register a callback to check if the speed is fast enough to damage the car
accelerate.tap("DamagePlugin".(newSpeed) = > {
  if (newSpeed > 300) {
    console.log("DamagePlugin"."It was going so fast, the car was falling apart..."); }});// Trigger the acceleration event to see the effect
accelerate.call(500);
Copy the code

Then run to see if the three callbacks are executed in sequence when the acceleration event occurs:

Tap takes two arguments. The first argument is a string, which is nothing more than a comment, and the second argument is a callback function that executes the specific logic of the event.

accelerate.tap("LoggerPlugin".(newSpeed) = >
  console.log("LoggerPlugin".` accelerated to${newSpeed}`));Copy the code

Webpack plguin is implemented with tapable, and the first parameter is usually the name of the plugin:

In webPack plugin, the developer is not required to trigger the event, but the Webpack itself will trigger different events at different stages, such as beforeRun, run, etc. Plguin developers are more concerned with what to do when these events occur, i.e. registering their own callbacks on these events.

SyncBailHook

SyncHook is a simple published-subscribe model. SyncBailHook adds a little bit of flow control to this model. Bail is a Bail, and the effect is to interrupt the process if a previous callback returns a non-undefined value. For example, if we change the SyncHook of the previous example to SyncBailHook, and add some logic to the speeding detection plugin, it will return an error when speeding, and the DamagePlugin will not execute:

const { SyncBailHook } = require("tapable");    // 使用的是SyncBailHook

// Instantiate an accelerated hook
const accelerate = new SyncBailHook(["newSpeed"]);

accelerate.tap("LoggerPlugin".(newSpeed) = >
  console.log("LoggerPlugin".` accelerated to${newSpeed}`));// Register a callback to check for overspeed
// An error is returned if the speed is too high
accelerate.tap("OverspeedPlugin".(newSpeed) = > {
  if (newSpeed > 120) {
    console.log("OverspeedPlugin"."You are speeding!!");

    return new Error('You were speeding!! '); }}); accelerate.tap("DamagePlugin".(newSpeed) = > {
  if (newSpeed > 300) {
    console.log("DamagePlugin"."It was going so fast, the car was falling apart..."); }}); accelerate.call(500);
Copy the code

Then run it again:

As you can see, the DamagePlugin is blocked because OverspeedPlugin returns a value that is not undefined.

SyncWaterfallHook

SyncWaterfallHook is also based on SyncHook with a bit of flow control. As mentioned above, Waterfall implements the effect of passing the return value of the last callback to the next one as a parameter. So arguments passed through call are only passed to the first callback function, subsequent callbacks accept the return value of the previous callback, and the return value of the last callback is returned to the outermost callback as the return value of call:

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

const accelerate = new SyncWaterfallHook(["newSpeed"]);

accelerate.tap("LoggerPlugin".(newSpeed) = > {
  console.log("LoggerPlugin".` accelerated to${newSpeed}`);

  return "LoggerPlugin";
});

accelerate.tap("Plugin2".(data) = > {
  console.log('The last plugin was:${data}`);

  return "Plugin2";
});

accelerate.tap("Plugin3".(data) = > {
  console.log('The last plugin was:${data}`);

  return "Plugin3";
});

const lastPlugin = accelerate.call(100);

console.log('The last plugin is:${lastPlugin}`);
Copy the code

Here’s how it works:

SyncLoopHook

SyncLoopHook is an extension of SyncHook’s looping logic, which means that if a plugin returns true, it will continue executing until it returns undefined:

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

const accelerate = new SyncLoopHook(["newSpeed"]);

accelerate.tap("LoopPlugin".(newSpeed) = > {
  console.log("LoopPlugin".'cycle speeds up to${newSpeed}`);

  return new Date().getTime() % 5! = =0 ? true : undefined;
});

accelerate.tap("LastPlugin".(newSpeed) = > {
  console.log("The cycle of acceleration is finally over.");
});

accelerate.call(100);
Copy the code

The execution effect is as follows:

Asynchronous API

The asynchronous API is in contrast to the previous synchronous API, where all callbacks are executed sequentially and synchronously, with all the synchronous code inside each callback. In a real project, however, you might want to handle asynchro within a callback, or you might want multiple callbacks to execute in Parallel, also known as Parallel. These requirements require the use of asynchronous apis. The main asynchronous apis are these:

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

Since we’re talking about asynchrony, we definitely need asynchrony, and Tapable supports both callback and Promise asynchrony. So in addition to registering callbacks with the previous TAP, these asynchronous apis also have two methods for registering callbacks: tapAsync and tapPromise, and the corresponding methods for triggering events are callAsync and Promise. Let’s take a look at each API separately:

AsyncParallelHook

AsyncParallelHook AsyncParallelHook (tapAsync) AsyncParallelHook (tapAsync) AsyncParallelHook (tapAsync)

TapAsync and callAsync

Again, the car accelerated, but it didn’t accelerate as fast. It took one second for the car to accelerate, and then we tested for speed and damage at two seconds. To see the parallel effect, we recorded the time of the whole process from start to end:

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

const accelerate = new AsyncParallelHook(["newSpeed"]);

console.time("total time"); // Record the start time

// Note that tapAsync is required to register asynchronous events
// The last argument received is done, which is called to indicate that the current task is finished
accelerate.tapAsync("LoggerPlugin".(newSpeed, done) = > {
  // Acceleration takes 1 second to complete
  setTimeout(() = > {
    console.log("LoggerPlugin".` accelerated to${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin".(newSpeed, done) = > {
  // Check for overspeed after 2 seconds
  setTimeout(() = > {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin"."You are speeding!!");
    }
    done();
  }, 2000);
});

accelerate.tapAsync("DamagePlugin".(newSpeed, done) = > {
  // Check for damage after 2 seconds
  setTimeout(() = > {
    if (newSpeed > 300) {
      console.log("DamagePlugin"."It was going so fast, the car was falling apart...");
    }

    done();
  }, 2000);
});

accelerate.callAsync(500.() = > {
  console.log("Mission accomplished.");
  console.timeEnd("total time"); // Record the total elapsed time
});

Copy the code

Note that tapAsync is used to register the callback, and the last argument in the callback function is passed to Done, which you can call to inform Tapable that the task is complete. CallAsync is used to trigger the task, which also receives a function that can be used to handle the operation that needs to be performed when all the tasks are complete. So the result of the above run is:

As you can see from this result, the final elapsed time is approximately 2 seconds, which is the longest single task of the three tasks, rather than the total time of the three tasks, which implements the effect of Parallel.

TapPromise and promise

Promise is popular now, so Tapable is also supported, and the implementation is the same, just written differently. To use tapPromise, the callback that needs to be registered returns a Promise, and the trigger event also needs to use promise. The processing of the execution after the task runs can be directly used then, so the above code is changed to:

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

const accelerate = new AsyncParallelHook(["newSpeed"]);

console.time("total time"); // Record the start time

// Note that tapPromise is required to register asynchronous events
// The callback returns a promise
accelerate.tapPromise("LoggerPlugin".(newSpeed) = > {
  return new Promise((resolve) = > {
    // Acceleration takes 1 second to complete
    setTimeout(() = > {
      console.log("LoggerPlugin".` accelerated to${newSpeed}`);

      resolve();
    }, 1000);
  });
});

accelerate.tapPromise("OverspeedPlugin".(newSpeed) = > {
  return new Promise((resolve) = > {
    // Check for overspeed after 2 seconds
    setTimeout(() = > {
      if (newSpeed > 120) {
        console.log("OverspeedPlugin"."You are speeding!!");
      }
      resolve();
    }, 2000);
  });
});

accelerate.tapPromise("DamagePlugin".(newSpeed) = > {
  return new Promise((resolve) = > {
    // Check for damage after 2 seconds
    setTimeout(() = > {
      if (newSpeed > 300) {
        console.log("DamagePlugin"."It was going so fast, the car was falling apart...");
      }

      resolve();
    }, 2000);
  });
});

// Trigger events use promise, and then directly handles the final result
accelerate.promise(500).then(() = > {
  console.log("Mission accomplished.");
  console.timeEnd("total time"); // Record the total elapsed time
});
Copy the code

The logic and result of this code is the same as above, but it is written differently:

TapAsync and tapPromise are mixed

Since Tapable supports both asynchronous writing, can the two writing methods be mixed? Let’s try it:

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

const accelerate = new AsyncParallelHook(["newSpeed"]);

console.time("total time"); // Record the start time

// Make a promise
accelerate.tapPromise("LoggerPlugin".(newSpeed) = > {
  return new Promise((resolve) = > {
    // Acceleration takes 1 second to complete
    setTimeout(() = > {
      console.log("LoggerPlugin".` accelerated to${newSpeed}`);

      resolve();
    }, 1000);
  });
});

// Async
accelerate.tapAsync("OverspeedPlugin".(newSpeed, done) = > {
  // Check for overspeed after 2 seconds
  setTimeout(() = > {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin"."You are speeding!!");
    }
    done();
  }, 2000);
});

// Use promise to trigger events
// accelerate.promise(500).then(() => {
// console.log(" Task complete ");
// console.timeEnd("total time"); // Record the total elapsed time
// });

// Use callAsync to fire events
accelerate.callAsync(500.() = > {
  console.log("Mission accomplished.");
  console.timeEnd("total time"); // Record the total elapsed time
});
Copy the code

The result of this code is the same whether I use promise or callAsync to trigger the event, so there should be a compatible conversion inside tapable. The two can be used interchangeably:

Since tapAsync and tapPromise are just written differently, I’ll just use tapAsync for the rest of my examples.

AsyncParallelBailHook

SyncBailHook SyncBailHook is a Bail function that blocks the execution of a task if it returns undefined. If a task returns a value other than undefined, the final callback will be executed immediately and the Bail task return value will be retrieved. We stagger the execution time of the above three tasks, namely 1 second, 2 seconds and 3 seconds respectively, and then trigger Bail in the 2-second task to see the effect:

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

const accelerate = new AsyncParallelBailHook(["newSpeed"]);

console.time("total time"); // Record the start time

accelerate.tapAsync("LoggerPlugin".(newSpeed, done) = > {
  // Acceleration takes 1 second to complete
  setTimeout(() = > {
    console.log("LoggerPlugin".` accelerated to${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin".(newSpeed, done) = > {
  // Check for overspeed after 2 seconds
  setTimeout(() = > {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin"."You are speeding!!");
    }

    Done for this task returns an error
    // Note that the first argument is a canonical error for node callback
    // The second argument is the Bail return value
    done(null.new Error("You are speeding!!"));
  }, 2000);
});

accelerate.tapAsync("DamagePlugin".(newSpeed, done) = > {
  // Check for damage after 3 seconds
  setTimeout(() = > {
    if (newSpeed > 300) {
      console.log("DamagePlugin"."It was going so fast, the car was falling apart...");
    }

    done();
  }, 3000);
});

accelerate.callAsync(500.(error, data) = > {
  if (data) {
    console.log("Task execution error:", data);
  } else {
    console.log("Mission accomplished.");
  }
  console.timeEnd("total time"); // Record the total elapsed time
});

Copy the code

As you can see, on task 2, the final callback will execute immediately because it returned an error, but since task 3 has been synchronized before, it will still complete itself, but it will not affect the final result:

AsyncSeriesHook

AsyncSeriesHook is an asynchronous serial hook. If there are multiple tasks, each task is serial, but the task itself may be asynchronous, the next task must wait for the previous task to finish before starting:

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

const accelerate = new AsyncSeriesHook(["newSpeed"]);

console.time("total time"); // Record the start time

accelerate.tapAsync("LoggerPlugin".(newSpeed, done) = > {
  // Acceleration takes 1 second to complete
  setTimeout(() = > {
    console.log("LoggerPlugin".` accelerated to${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin".(newSpeed, done) = > {
  // Check for overspeed after 2 seconds
  setTimeout(() = > {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin"."You are speeding!!");
    }
    done();
  }, 2000);
});

accelerate.tapAsync("DamagePlugin".(newSpeed, done) = > {
  // Check for damage after 2 seconds
  setTimeout(() = > {
    if (newSpeed > 300) {
      console.log("DamagePlugin"."It was going so fast, the car was falling apart...");
    }

    done();
  }, 2000);
});

accelerate.callAsync(500.() = > {
  console.log("Mission accomplished.");
  console.timeEnd("total time"); // Record the total elapsed time
});

Copy the code

The code of each task is the same as that of the AsyncParallelHook task, but the use of different hooks is different. The AsyncSeriesHook task is executed sequentially, and the next task can only be started after the completion of the previous task. Therefore, the final total time is the total time of all tasks. The above example is 1 + 2 + 2, that is, 5 seconds:

AsyncSeriesBailHook

AsyncSeriesBailHook: AsyncSeriesBailHook: AsyncSeriesBailHook: AsyncSeriesBailHook: AsyncSeriesBailHook: AsyncSeriesBailHook: AsyncSeriesBailHook: AsyncSeriesBailHook: AsyncSeriesBailHook: AsyncSeriesBailHook: AsyncSeriesBailHook: AsyncSeriesBailHook: AsyncSeriesBailHook

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

const accelerate = new AsyncSeriesBailHook(["newSpeed"]);

console.time("total time"); // Record the start time

accelerate.tapAsync("LoggerPlugin".(newSpeed, done) = > {
  // Acceleration takes 1 second to complete
  setTimeout(() = > {
    console.log("LoggerPlugin".` accelerated to${newSpeed}`);

    done();
  }, 1000);
});

accelerate.tapAsync("OverspeedPlugin".(newSpeed, done) = > {
  // Check for overspeed after 2 seconds
  setTimeout(() = > {
    if (newSpeed > 120) {
      console.log("OverspeedPlugin"."You are speeding!!");
    }

    Done for this task returns an error
    // Note that the first argument is a canonical error for node callback
    // The second argument is the Bail return value
    done(null.new Error("You are speeding!!"));
  }, 2000);
});

accelerate.tapAsync("DamagePlugin".(newSpeed, done) = > {
  // Check for damage after 2 seconds
  setTimeout(() = > {
    if (newSpeed > 300) {
      console.log("DamagePlugin"."It was going so fast, the car was falling apart...");
    }

    done();
  }, 2000);
});

accelerate.callAsync(500.(error, data) = > {
  if (data) {
    console.log("Task execution error:", data);
  } else {
    console.log("Mission accomplished.");
  }
  console.timeEnd("total time"); // Record the total elapsed time
});

Copy the code

The difference between AsyncSeriesBailHook and AsyncParallelBailHook is that after AsyncSeriesBailHook is blocked, the following tasks can be completely blocked because the AsyncSeriesBailHook has not started yet. So I’m still going to do it, but I don’t care about the result.

AsyncSeriesWaterfallHook

Waterfall works by transferring the results of the previous task to the next task, and other options include AsyncSeriesHook.

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

const accelerate = new AsyncSeriesWaterfallHook(["newSpeed"]);

console.time("total time"); // Record the start time

accelerate.tapAsync("LoggerPlugin".(newSpeed, done) = > {
  // Acceleration takes 1 second to complete
  setTimeout(() = > {
    console.log("LoggerPlugin".` accelerated to${newSpeed}`);

    // Note that the first argument to done is treated as error
    // The second argument is the one passed to the next task
    done(null."LoggerPlugin");
  }, 1000);
});

accelerate.tapAsync("Plugin2".(data, done) = > {
  setTimeout(() = > {
    console.log('The last plugin was:${data}`);

    done(null."Plugin2");
  }, 2000);
});

accelerate.tapAsync("Plugin3".(data, done) = > {
  setTimeout(() = > {
    console.log('The last plugin was:${data}`);

    done(null."Plugin3");
  }, 2000);
});

accelerate.callAsync(500.(error, data) = > {
  console.log("The last plug-in is :", data);
  console.timeEnd("total time"); // Record the total elapsed time
});

Copy the code

The running effect is as follows:

conclusion

The examples of this article have all been uploaded to GitHub, you can take them down for reference:Github.com/dennis-jian…

  1. tapableiswebpackimplementationpluginThe core library for himwebpackProvides a variety of event handling and flow controlHook.
  2. theseHookThere are mainly synchronous (Sync) and asynchronous (Async), but also provides blocking (Bail), waterfall (Waterfall), loop (LoopFor asynchronous processes, it also provides parallelism (Paralle) and serial (Series) Two control modes.
  3. tapableThe core principle is still eventPublish and subscribe modelHe usetapTo register the event, usecallTo trigger the event.
  4. asynchronoushookTwo formats are supported: callback and harmonicPromise, registration and triggering events are used separatelytapAsync/callAsyncandtapPromise/promise.
  5. asynchronoushookWhen writing a callback, note that the first argument to the callback function defaults to an error, and the second argument is the data being passed outnodeCallback style.

This article mainly describes the use of tapable, later I will write an article to analyze his source code, click a concern not lost, haha ~

At the end of this article, thank you for your precious time to read this article. If this article gives you a little help or inspiration, please do not spare your thumbs up and GitHub stars. Your support is the motivation of the author’s continuous creation.

Welcome to follow my public numberThe big front end of the attackThe first time to obtain high quality original ~

“Front-end Advanced Knowledge” series:Juejin. Cn/post / 684490…

“Front-end advanced knowledge” series article source code GitHub address:Github.com/dennis-jian…