preface

Webpack is a static module wrapper for modern JavaScript applications. It is an essential tool for the automation and optimization of front-end projects. The Loader and plugin of Webpack are contributed by Webpack developers and community developers. And at present there is no more systematic development documents, want to write loader and plug-in must understand the principle of Webpack, that is, to understand the source code of Webpack, Tapable is the core library that Webpack depends on, it can be said that if you do not understand tapable, you cannot understand the source code of Webpack. So this article will parse and simulate the classes provided by Tapable.

Tapable introduction

Webpack is essentially an event flow mechanism, and its workflow is to connect various plug-ins in series, and the core of this is tapable, the most core of Webpack, The Compiler responsible for compiling and the Compilation responsible for creating bundles are instances of the tapable constructor.

Methods that start with Sync and Async and end with Hook are classes in the Tapable core library that provide different event flow execution mechanisms called hooks.

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

The above general direction of the “hooks” that implement the event flow mechanism can be divided into two categories, “synchronous” and “asynchronous”. “asynchronous” is further divided into two categories, “parallel” and “serial”, while “synchronous” hooks are serial.

Sync type hook

1, SyncHook

SyncHook executes in serial synchronism, regardless of the return value of event handlers, and executes all event handlers in the order in which they were registered after an event is triggered.

// SyncHook hook const {SyncHook} = require("tapable"); Let syncHook = new syncHook (["name", "age"]); Syncook.tap ("1", (name, age) => console.log("1", name, age)); syncHook.tap("2", (name, age) => console.log("2", name, age)); syncHook.tap("3", (name, age) => console.log("3", name, age)); Call ("panda", 18); // Trigger the event to make the listener execute syncook. call("panda", 18); // 1 panda 18 // 2 panda 18 // 3 panda 18Copy the code

SyncHook is a class destructed by Tapable, which needs to create an instance before registering an event. When creating an instance, it supports passing in an array that stores the parameters passed in when the event is triggered. The tap method of the instance is used to register the event, and it supports passing in two parameters. Typically used in Webpack to store the name of the plug-in that corresponds to the event (optionally annotated), the second argument is the event handler, and the function argument is the parameter passed in when the call method fires the event.

Constructor (args) {this.args = args; // Constructor (args) {this.args = args; this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(... Args) {if (args. Length < this.args. Length) throw new Error(" Parameter not enough "); Args = args. Slice (0, this.args. Length); args = args. This.tasks. forEach(task => task(... args)); }}Copy the code

The Tasks array is used to store event handlers. When the call method is called with more parameters than the array used to create the SyncHook instance, the extra parameters can be treated as undefined, or an exception can be thrown if the parameters are insufficient.

2, SyncBailHook

SyncBailHook is also executed sequentially, and if the event handler executes with a return value that is not null (i.e., undefined), the remaining unexecuted event handlers (such as the name of the class, meaning insurance) are skipped.

Const {SyncBailHook} = require("tapable"); Let syncBailHook = new syncBailHook (["name", "age"]); Syncbailhook.tap ("1", (name, age) => console.log("1", name, age)); syncBailHook.tap("2", (name, age) => { console.log("2", name, age); return "2"; }); syncBailHook.tap("3", (name, age) => console.log("3", name, age)); // Trigger the event to make the listener execute syncbailhook. call("panda", 18); // 1 panda 18 // 2 panda 18Copy the code

SyncHook and SyncBailHook are logically different except for the call method, which results in different event execution mechanisms. For the other “hooks”, this is also the difference between call and SyncBailHook.

Class SyncBailHook {constructor(args) {this.args = args; this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(... Args = args. Slice (0, this.args. Length); args = args. // Execute the event handlers in turn, stop down if the return value is not null, let I = 0, ret; do { ret = this.tasks[i++](... args); } while (! ret); }}Copy the code

In the call method of the code above, we set the return value to RET, continue the loop if there is no return value after the first execution, and immediately stop the loop if there is a return value, which is “safe”.

3, SyncWaterfallHook

SyncWaterfallHook is executed in serial synchronism, and the return value of the last event handler is passed as an argument to the next event handler, and so on. Because of this, only the arguments of the first event handler can be passed through call, and the return value of call is the return value of the last event handler.

// SyncWaterfallHook const {SyncWaterfallHook} = require("tapable"); Let syncWaterfallHook = new syncWaterfallHook (["name", "age"]); SyncWaterfallHook. Tap ("1", (name, age) => {console.log("1", name, age); return "1"; }); syncWaterfallHook.tap("2", data => { console.log("2", data); return "2"; }); syncWaterfallHook.tap("3", data => { console.log("3", data); return "3" }); Let ret = syncWaterfallHook. Call ("panda", 18); console.log("call", ret); // 1 panda 18 // 2 1 // 3 2 // call 3Copy the code

SyncWaterfallHook (SyncWaterfallHook, SyncWaterfallHook, SyncWaterfallHook, SyncWaterfallHook, SyncWaterfallHook, SyncWaterfallHook) Next, take a look at the implementation of the SyncWaterfallHook class.

Constructor (args) {this.args = args; this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(... Args = args. Slice (0, this.args. Length); args = args. Let [first,...others] = this.tasks; return reduce((ret, task) => task(ret), first(... args)); }}Copy the code

The call logic in the above code is to split the tasks storing event handlers into two parts, namely the first event handler and the array storing other event handlers, and merge them using reduce. The return value of the first event handler is taken as the initial value of merge. The remaining event handlers are called in turn and passed the return value of the previous merge.

4, SyncLoopHook

SyncLoopHook executes in serial synchronism. The event handler returns true to continue the loop, and returns undefined to end the loop. SyncLoopHook is different from SyncBailHook. SyncBailHook only decides whether to proceed to the next event handler, whereas SyncLoopHook loops through each event handler until it returns undefined before continuing to proceed to the next event handler.

// SyncLoopHook hook const {SyncLoopHook} = require("tapable"); Let syncLoopHook = new syncLoopHook (["name", "age"]); // let total1 = 0; let total2 = 0; Syncloophook. tap("1", (name, age) => {console.log("1", name, age, total1); return total1++ < 2 ? true : undefined; }); syncLoopHook.tap("2", (name, age) => { console.log("2", name, age, total2); return total2++ < 2 ? true : undefined; }); syncLoopHook.tap("3", (name, age) => console.log("3", name, age)); // Trigger the event to make the listener execute syncloophook. call("panda", 18); // 1 panda 18 0 // 1 panda 18 1 // 1 panda 18 2 // 2 panda 18 0 // 2 panda 18 1 // 2 panda 18 2 // 3 panda 18Copy the code

You can clearly see the above resultsSyncLoopHookWith the caveat that the return value must be strictlytrueTo trigger a loop that executes the current event handler multiple times and must strictly returnundefinedIf the return value of the event handler is nottrueIs notundefined, will be an infinite loop.

Now that you know how SyncLoopHook works, let’s look at how SyncLoopHook’s call method is implemented.

Class SyncLoopHook {constructor(args) {this.args = args; this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(... Args = args. Slice (0, this.args. Length); args = args. This.tasks. forEach(task => {let ret; task => {let ret; do { ret = this.task(... args); } while (ret === true || ! (ret === undefined)); }); }}Copy the code

As you can see from the above code, the SyncLoopHook call method is implemented more like a combination of the SyncHook and SyncBailHook call methods. The outer layer loops through the queue of tasks event handlers, and the inner layer loops through the return value. Controls the number of times each event handler is executed.

Note: Plugins executed under Sync type hooks are executed sequentially and can only be usedtabRegistration.

Async type of hook

AsyncType can be usedtap,tapSynctapPromiseRegister different types of plug-in “hooks”, respectively throughcall,callAsyncpromiseMethod calls, which we will address belowAsyncParallelHookAsyncSeriesHookasyncpromiseThe two methods are introduced and simulated respectively.

1, AsyncParallelHook

Asyncparallelhooks are executed asynchronously in parallel, with events registered with tapAsync, fired with callAsync, registered with tapPromise, fired with Promise (the return value can call the then method).

(1) tapAsync/callAsync

The last argument to callAsync is a callback function that is executed after all event handlers have finished executing.

// AsyncParallelHook hook: tapAsync/callAsync const {AsyncParallelHook} = require("tapable"); Let asyncParallelHook = new asyncParallelHook (["name", "age"]); // Register event console.time("time"); asyncParallelHook.tapAsync("1", (name, age, done) => { settimeout(() => { console.log("1", name, age, new Date()); done(); }, 1000); }); asyncParallelHook.tapAsync("2", (name, age, done) => { settimeout(() => { console.log("2", name, age, new Date()); done(); }, 2000); }); asyncParallelHook.tapAsync("3", (name, age, done) => { settimeout(() => { console.log("3", name, age, new Date()); done(); console.timeEnd("time"); }, 3000); }); / / triggers, let the monitor function performs asyncParallelHook. CallAsync (" panda ", 18, () = > {the console. The log (" complete "); }); // 1 Panda 18 2018-08-07T10:38:32.675z // 2 Panda 18 2018-08-07T10:38:33.674z // 3 Panda 18 2018-08-07T10:38:34.674z // 3 Panda 18 2018-08-07T10:38:34.674z // Complete / / time: 3005.060 msCopy the code

Asynchronous parallelism means that the maximum duration of the asynchronous operation of the three timers in the event handler is3s, while the three event handlers are close in total3sSo all three event handlers are executed almost simultaneously without waiting.

The last parameter of all tabAsync event handlers is a callback called done. If each event handler calls done after the asynchronous code has finished executing, callAsync is guaranteed to execute after all asynchronous functions have finished executing. Let’s look at how callAsync is implemented.

TapAsync /callAsync class AsyncParallelHook {constructor(args) {this.args = args; this.tasks = []; } tabAsync(name, task) { this.tasks.push(task); } callAsync(... Args) {let finalCallback = args.pop(); Args = args. Slice (0, this.args. Length); args = args. CallAsync let I = 0; callAsync let I = 0; let done = () => { if (++i === this.tasks.length) { finalCallback(); }}; This.tasks. forEach(task => task(... args, done)); }}Copy the code

In callAsync, take the last argument (the callback to execute after all the event handlers have finished executing) and define the done function to determine whether the callback is executed by comparing I with the length of the tasks array that stores the event handlers. The loop executes each event handler and passes done as the last argument, so when an asynchronous operation inside each event handler completes, done is executed to check whether callAsync is the right callback to execute. When all event handlers meet the condition that I and Length are equal inside the Done function, the callAsync callback is invoked.

(2) tapPromise/promise

To register an event using tapPromise, there is a requirement for the event handler that it returns a Promise instance, and the Promise method returns a Promise instance, The callAsync callback is replaced by then in the Promise method.

// AsyncParallelHook hook: tapPromise/promise use const {AsyncParallelHook} = require("tapable"); Let asyncParallelHook = new asyncParallelHook (["name", "age"]); // Register event console.time("time"); asyncParallelHook.tapPromise("1", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("1", name, age, new Date()); resolve("1"); }, 1000); }); }); asyncParallelHook.tapPromise("2", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("2", name, age, new Date()); resolve("2"); }, 2000); }); }); asyncParallelHook.tapPromise("3", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("3", name, age, new Date()); resolve("3"); console.timeEnd("time"); }, 3000); }); }); / / triggers, let the monitor function performs asyncParallelHook. Promise (" panda ", 18). Then (ret = > {the console. The log (ret); }); // 1 Panda 18 2018-08-07T12:17:21.741z // 2 Panda 18 2018-08-07T12:17:22.736z // 3 Panda 18 2018-08-07T12:17:23.739z // Time: 3006.542ms // ['1', '2', '3']Copy the code

Each of the above event handlers of tapPromise registered events returns a Promise instance and passes the return value to the resolve method. When the event is triggered by calling the Promise method, if all the Promise instances returned by the event handlers are successful, The result is stored in an array and passed as an argument to the successful callback in the Promise’s then method, and if there is a failure, the failed result is returned as an argument to the failed callback.

TapPromise /promise class AsyncParallelHook {constructor(args) {this.args = args; this.tasks = []; } tapPromise(name, task) { this.tasks.push(task); } promise(... Args = args. Slice (0, this.args. Length); args = args. All (this.tasks.map(task => task(...)) // Convert all event handlers to Promise instances and execute all Promise return promises.all (this.tasks.map(task => task(... args))); }}Copy the code

In fact, it can be guessed from the above description of the use of tapPromise and promise that the logic of the promise method is implemented through promise.all.

2, AsyncSeriesHook

AsyncSeriesHook is an asynchronous serial execution, the same as AsyncParallelHook. Events registered by tapAsync are triggered by callAsync, events registered by tapPromise are triggered by Promise, You can call the then method.

(1) tapAsync/callAsync

Similar to AsyncParallelHook’s callAsync method, AsyncSeriesHook’s callAsync method also executes the callAsync callback function by passing in a callback function after all event handlers have finished executing.

Const {AsyncSeriesHook} = require("tapable"); // AsyncSeriesHook: tapAsync/callAsync const {AsyncSeriesHook} = require("tapable"); Let asyncSeriesHook = new asyncSeriesHook (["name", "age"]); // Register event console.time("time"); asyncSeriesHook.tapAsync("1", (name, age, next) => { settimeout(() => { console.log("1", name, age, new Date()); next(); }, 1000); }); asyncSeriesHook.tapAsync("2", (name, age, next) => { settimeout(() => { console.log("2", name, age, new Date()); next(); }, 2000); }); asyncSeriesHook.tapAsync("3", (name, age, next) => { settimeout(() => { console.log("3", name, age, new Date()); next(); console.timeEnd("time"); }, 3000); }); / / triggers, let the monitor function performs asyncSeriesHook. CallAsync (" panda ", 18, () = > {the console. The log (" complete "); }); // 1 Panda 18 2018-08-07T14:40:52.896z // 2 Panda 18 2018-08-07T14:40:54.901z // 3 Panda 18 2018-08-07T14:40:57.901z // Complete / / time: 6008.790 msCopy the code

Asynchronous serial refers to the asynchronous execution time of the three timers in the event handler function1s,2s3s, while the total execution time of the three event handlers is close6s, so the three event handlers are queued and must be executed one by one. The current event handler can be executed before the next one is executed.

AsyncSeriesHook (tabAsync) : next (tabAsync) : done (tabAsync) : Done (tabAsync) : Done (tabAsync) The difference lies in the “parallel” and “serial” implementations of the callAsync methods of AsyncSeriesHook and AsyncParallelHook.

AsyncSeriesHook class: tapAsync/callAsync Class AsyncSeriesHook {constructor(args) {this.args = args; this.tasks = []; } tabAsync(name, task) { this.tasks.push(task); } callAsync(... Args) {let finalCallback = args.pop(); Args = args. Slice (0, this.args. Length); args = args. // If next is not called, let I = 0; // If next is not called, let I = 0; let next = () => { let task = this.tasks[i++]; task ? task(... args, next) : finalCallback(); }; next(); }}Copy the code

AsyncParallelHook is a loop that executes all event handlers in sequence. The done method only performs callAsync callbacks to check whether the condition has been met. If a middle event handler does not call done, it does not call callAsync callbacks. But all event handlers are executed.

The next execution mechanism of AsyncSeriesHook is more like the middleware in Express and Koa. If next is not called in the callback of the registered event, the event will be “stuck” in the position of the event handler that did not call Next. That is, the subsequent event handlers will not continue until they all call next, and next in the last event handler determines whether callAsync’s callback will be called.

(2) tapPromise/promise

Like AsyncParallelHook, the event handler for tapPromise registers an event needs to return a Promise instance, and the Promise method finally returns a Promise instance.

// AsyncSeriesHook hook: tapPromise/promise use const {AsyncSeriesHook} = require("tapable"); Let asyncSeriesHook = new asyncSeriesHook (["name", "age"]); // Register event console.time("time"); asyncSeriesHook.tapPromise("1", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("1", name, age, new Date()); resolve("1"); }, 1000); }); }); asyncSeriesHook.tapPromise("2", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("2", name, age, new Date()); resolve("2"); }, 2000); }); }); asyncParallelHook.tapPromise("3", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("3", name, age, new Date()); resolve("3"); console.timeEnd("time"); }, 3000); }); }); AsyncSeriesHook. Promise ("panda", 18). Then (ret => {console.log(ret); }); // 1 Panda 18 2018-08-07T14:45:52.896z // 2 Panda 18 2018-08-07T14:45:54.901z // 3 Panda 18 2018-08-07T14:45:57.901z // 3 Panda 18 2018-08-07T14:45:57.901z // Time: 6014.296ms // ['1', '2', '3']Copy the code

All the event handlers return instances of Promise. If you want to serialize, you need to have each returned Promise instance call THEN and execute the next event handler in that THEN. This ensures that the next event handler will not execute until the last one has finished.

TapPromise /promise class AsyncSeriesHook {constructor(args) {this.args = args; this.tasks = []; } tapPromise(name, task) { this.tasks.push(task); } promise(... Args = args. Slice (0, this.args. Length); args = args. // Let the next event handler execute let [first,...others] = this.tasks; return others.reduce((promise, task) => { return promise.then(() => task(... args)); }, first(... args)); }}Copy the code

The “serial” in the above code is realized by using reduce merge. First, divide the array of tasks storing all the event handlers into two parts, the first event handler and the array of others storing other event handlers, merge the others. Use the Promise instance returned by the first event handler as the initial value of the merge, so that the last value during the merge is always the Promise instance returned by the previous event handler, call the THEN method directly, and execute the next event handler in the then callback. Until the merging is completed, the Promise instance returned by Reduce is taken as the return value of the Promise method, then the Promise method is implemented and then is invoked to realize the subsequent logic.

Complement other asynchronous hooks

In the above Async type of “hook”, we only focus on “serial” and “parallel” (AsyncSeriesHook and AsyncSeriesHook), as well as callback and Promise two ways to register and trigger events. There are other asynchronous hooks that have certain characteristics that we haven’t analyzed because their mechanics are very similar to their synchronous counterparts.

AsyncParallelBailHook (AsyncSeriesBailHook) ¶ AsyncParallelBailHook (AsyncSeriesBailHook) ¶ The implementation logic can combine AsyncParallelHook, AsyncSeriesHook, and SyncBailHook.

AsyncSeriesWaterfallHook AsyncSeriesWaterfallHook AsyncSeriesWaterfallHook AsyncSeriesWaterfallHook AsyncSeriesWaterfallHook AsyncSeriesWaterfallHook

conclusion

In tapable source code, the methods TAB, tapSync, tapPromise for registering events and the methods Call, callAsync, and Promise for triggering events are all compiled quickly by compile method. The implementation of these methods in this paper just follows the event processing mechanism of these “hooks” in tapable library to simulate, so as to facilitate our understanding of Tapable and pave the way for learning the principle of Webpack. In Webpack, The real purpose of these “hooks” is to connect plug-in to plug-in, loader to loader, “parallel” or “serial” execution, after we understand the event mechanism of these “hooks” in Tapable. Re-studying the source code of Webpack should give you some ideas.