In my last article, I wrote about the basic usage of Tapable, which we know is an enhanced version of the publishave-subscribe model, so this article would like to take a look at its source code. I read the source code for Tapable and found that it is quite abstract, and it would be confusing to dive directly into it. Therefore, this article will start with the simplest SyncHook and publish/subscribe model, and then gradually abstract it into its source code.
This article can run the example code has been uploaded to GitHub, we take down while playing while reading the article effect better: github.com/dennis-jian… .
SyncHook
Basic implementation of
The SyncHook was used in the last article, but I won’t expand on it here. The example he uses looks like this:
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!!"); }});// Trigger the acceleration event to see the effect
accelerate.call(500);
Copy the code
In fact, this usage is a basic publish-subscribe model. As I mentioned in my previous post on publish-subscribe, we can quickly implement a SyncHook similar to that:
class SyncHook {
constructor(args = []) {
this._args = args; // The received parameters are saved
this.taps = []; // An array of callbacks
}
// The tap instance method is used to register the callback
tap(name, fn) {
// The logic is simple, just save the callback parameters passed in
this.taps.push(fn);
}
The call instance method is used to trigger events that perform all callbacks
call(. args) {
// The logic is simple, just take the registered callbacks out and execute them one by one
const tapsLength = this.taps.length;
for(let i = 0; i < tapsLength; i++) {
const fn = this.taps[i]; fn(... args); }}}Copy the code
This code is very simple, it is a basic publish-subscribe model, using the same method as above, changing SyncHook export from Tapable to use our own:
// const { SyncHook } = require("tapable");
const { SyncHook } = require("./SyncHook");
Copy the code
It works the same way:
Note: The args we passed into the constructor is not used. Tapable uses it to dynamically generate the call body, as we’ll see later in code factories.
SyncBailHook
Basic implementation of
Here’s a basic implementation of SyncBailHook. SyncBailHook prevents subsequent callbacks from executing if the current callback returns a value that is not undefined. Here’s the basic usage:
const { SyncBailHook } = require("tapable"); // 使用的是SyncBailHook
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!! '); }});// Because the last callback returned a value that was not undefined
// This callback will no longer run
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
SyncBailHook’s implementation is similar to SyncHook’s, except that the call is executed differently. SyncBailHook detects the return value of each callback and terminates subsequent callbacks if it is not undefined.
class SyncBailHook {
constructor(args = []) {
this._args = args;
this.taps = [];
}
tap(name, fn) {
this.taps.push(fn);
}
// The rest of the code is the same as SyncHook, except that the call implementation is different
// Each return value needs to be checked, and if it is not undefined, the execution is terminated
call(. args) {
const tapsLength = this.taps.length;
for(let i = 0; i < tapsLength; i++) {
const fn = this.taps[i];
constres = fn(... args);if( res ! = =undefined) returnres; }}}Copy the code
Then change SyncBailHook from our own introduction:
// const { SyncBailHook } = require("tapable");
const { SyncBailHook } = require("./SyncBailHook");
Copy the code
It works the same way:
Abstract duplicate code
Now we only implement SyncHook and SyncBailHook. In the last article on usage, there are 9 hooks in total. If every Hook is implemented like the above, it is ok. But a closer look at the SyncHook and SyncBailHook classes shows that they are identical except for the call implementation, so as ambitious engineers, we can refer to this part of the repeated code as a base class: Hook.
The Hook class needs to contain some common code, and the different parts of call are implemented by the subclasses themselves. So a Hook class looks like this:
const CALL_DELEGATE = function(. args) {
this.call = this._createCall();
return this.call(... args); };// Hook is the base class of SyncHook and SyncBailHook
// The general structure is the same, the difference is the call
// Different subclasses call differently
// Tapable's Hook base class provides an abstract interface compile to dynamically generate call functions
class Hook {
constructor(args = []) {
this._args = args;
this.taps = [];
// Base class call is initialized as CALL_DELEGATE
This.call = _createCall() this.call = _createCall()
// We will talk about it later when we implement subclasses
this.call = CALL_DELEGATE;
}
// compile an abstract interface
// Implemented by subclasses, the base class compile cannot be called directly
compile(options) {
throw new Error("Abstract: should be overridden");
}
tap(name, fn) {
this.taps.push(fn);
}
// _createCall calls the subclass's implementation compile to generate the call method
_createCall() {
return this.compile({
taps: this.taps,
args: this._args, }); }}Copy the code
The official corresponding source code here: github.com/webpack/tap…
Subclass SyncHook implementation
Now that we have a Hook base class, our SyncHook needs to extend the base overwrite. Tapable does this without using class extends, but manually:
const Hook = require('./Hook');
function SyncHook(args = []) {
// Manually inherit the Hook first
const hook = new Hook(args);
hook.constructor = SyncHook;
// Then implement your own compile function
// compile should create a call function and return it
hook.compile = function(options) {
// The call function is implemented the same as before
const { taps } = options;
const call = function(. args) {
const tapsLength = taps.length;
for(let i = 0; i < tapsLength; i++) {
const fn = this.taps[i];
fn(...args);
}
}
return call;
};
return hook;
}
SyncHook.prototype = null;
Copy the code
Note: We initialize this.call as a CALL_DELEGATE function in the base Hook constructor for a reason, the main reason being to make sure this points correctly. Think about what would happen if instead of CALL_DELEGATE, we just called this.call = this._createcall (). Let’s analyze the execution process:
- When users use it, they must use it
new SyncHook()
“Is executedconst hook = new Hook(args);
new Hook(args)
Can be carried toHook
Constructor of, that is, will runthis.call = this._createCall()
- At that time
this
It points to the base classHook
An instance of thethis._createCall()
The base class will be calledthis.compile()
- Because of the base class
complie
The function is an abstract interface and can be called directly with an errorAbstract: should be overridden
.
How do we solve this problem by using this.call = CALL_DELEGATE?
- using
this.call = CALL_DELEGATE
After the base classHook
On thecall
It is simply assigned to a proxy function that will not be called immediately. - The same is true for users
new SyncHook()
, which will be executedHook
Constructor of Hook
The constructor will givethis.call
The assignment forCALL_DELEGATE
, but not immediately.new SyncHook()
Proceed with the method on the newly created instancehook.complie
Overridden as the correct method.- When the user calls
hook.call
“Will be implementedthis._createCall()
That’s going to be calledthis.complie()
- That’s called at this point
complie
It’s already overwritten correctly, so you get the right answer.
An implementation of a subclass SyncBailHook
The SyncBailHook subclass is similar to the SyncHook subclass, but the hook.compile subclass is different:
const Hook = require('./Hook');
function SyncBailHook(args = []) {
// The basic structure is the same as SyncHook
const hook = new Hook(args);
hook.constructor = SyncBailHook;
// Compile is implemented as a Bail version
hook.compile = function(options) {
const { taps } = options;
const call = function(. args) {
const tapsLength = taps.length;
for(let i = 0; i < tapsLength; i++) {
const fn = this.taps[i];
constres = fn(... args);if( res ! = =undefined) break; }}return call;
};
return hook;
}
SyncBailHook.prototype = null;
Copy the code
Abstract code factory
Above, we abstracted SyncHook and SyncBailHook to create a base class Hook to reduce repetitive code. All that subclasses need to implement is the complie method, but if we compare SyncHook to SyncBailHook’s complie method:
SyncHook:
hook.compile = function(options) {
const { taps } = options;
const call = function(. args) {
const tapsLength = taps.length;
for(let i = 0; i < tapsLength; i++) {
const fn = this.taps[i];
fn(...args);
}
}
return call;
};
Copy the code
SyncBailHook:
hook.compile = function(options) {
const { taps } = options;
const call = function(. args) {
const tapsLength = taps.length;
for(let i = 0; i < tapsLength; i++) {
const fn = this.taps[i];
constres = fn(... args);if( res ! = =undefined) returnres; }}return call;
};
Copy the code
We found that both complie were very similar, with a lot of duplicate code, so Tapable abstracted the code factory HookCodeFactory to solve the problem. The function of HookCodeFactory is to generate the call function body returned by complie. HookCodeFactory also adopts the similar idea of Hook in its implementation, and also implements a base class HookCodeFactory first. Different hooks then inherit this class to implement their own code factories, such as SyncHookCodeFactory.
The method to create a function
Before diving further into the code factory, let’s review the way functions are created in JS. Generally, we have the following methods:
-
Function declaration
function add(a, b) { return a + b; } Copy the code
-
Functional expression
const add = function(a, b) { return a + b; } Copy the code
But in addition to these two methods, there is another, less commonly used method: use the Function constructor. For example, this function is created using the constructor:
const add = new Function('a'.'b'.'return a + b; ');
Copy the code
In the above form, the last argument is the body of the function, and the preceding arguments are the parameters of the function. The resulting function has the same effect as the function expression, and can be called as follows:
add(1.2); // The result is 3
Copy the code
Note: The above a and b parameters can also be separated by a comma:
const add = new Function('a, b'.'return a + b; '); // This will have the same effect as above
Copy the code
Of course, functions do not have to have arguments. Functions without arguments can be created as follows:
const sayHi = new Function('alert("Hello")');
sayHi(); // Hello
Copy the code
What’s the difference between creating a function like this and declaring and expressing a function earlier? One of the biggest features of using the Function constructor to create a Function is that the Function body is a string, which means we can generate the string dynamically, thus generating the Function body dynamically. Because SyncHook and SyncBailHook’s call functions are very similar, we can spell out their function bodies as if they were strings. To make this easier, Tapable’s resulting call function does not have a loop in it, but rather unwinds the loop as it is put together. For example, SyncHook spells out the body of the call function like this:
"use strict";
var _x = this._x;
var _fn0 = _x[0];
_fn0(newSpeed);
var _fn1 = _x[1];
_fn1(newSpeed);
Copy the code
The _x in the above code is actually the taps array that holds the callbacks. It is renamed _x to save code size. As you can see from the code, the contents of _x, or TAPS, have been expanded and are pulled out one by one.
The body of the call function generated by SyncBailHook looks like this:
"use strict";
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(newSpeed);
if(_result0 ! = =undefined) {
return _result0;
;
} else {
var _fn1 = _x[1];
var _result1 = _fn1(newSpeed);
if(_result1 ! = =undefined) {
return _result1;
;
} else{}}Copy the code
The main logic of this generated code is the same as that of SyncHook, which expands _x. The difference is that SyncBailHook checks the result of each execution. If the result is not undefined, it will return and the subsequent callback will not be executed.
Create the code factory base class
For this purpose, our code factory base class should be able to generate the most basic call function body. Let’s write a basic HookCodeFactory that currently generates only the SyncHook call body:
class HookCodeFactory {
constructor() {
// The constructor defines two variables
this.options = undefined;
this._args = undefined;
}
The init function initializes variables
init(options) {
this.options = options;
this._args = options.args.slice();
}
// deinit resets variables
deinit() {
this.options = undefined;
this._args = undefined;
}
// args is used to convert the array args passed in to the comma-separated form received by New Function
// ['arg1', 'args'] ---> 'arg1, arg2'
args() {
return this._args.join(",");
}
Setup is simply assigning _x to the generated code
setup(instance, options) {
instance._x = options.taps.map(t= > t);
}
// create Creates the final call function
create(options) {
this.init(options);
let fn;
// Expand TAPS directly to a flattened function call
const { taps } = options;
let code = ' ';
for (let i = 0; i < taps.length; i++) {
code += `
var _fn${i} = _x[${i}];
_fn${i}(The ${this.args()});
`
}
// Connect the expanded loop to the header
const allCodes = ` "use strict"; var _x = this._x; ` + code;
// Create a function with the parameters passed in and the generated function body
fn = new Function(this.args(), allCodes);
this.deinit(); // Reset the variable
return fn; // Return the generated function}}Copy the code
At the heart of the above code is the create function, which creates a call function on the fly and returns it, so SyncHook can create code directly using the factory:
// SyncHook.js
const Hook = require('./Hook');
const HookCodeFactory = require("./HookCodeFactory");
const factory = new HookCodeFactory();
// COMPILE calls factory to generate the call function
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
function SyncHook(args = []) {
const hook = new Hook(args);
hook.constructor = SyncHook;
// Use HookCodeFactory to create the final call function
hook.compile = COMPILE;
return hook;
}
SyncHook.prototype = null;
Copy the code
Get code factory supportSyncBailHook
Now our HookCodeFactory can only generate the simplest SyncHook code, so we need to improve it so that it can also generate the call body of SyncBailHook. You can scroll back and take a closer look at the differences between the two final generated code:
SyncBailHook
That needs to be executed each timeresult
If noundefined
It returnsSyncBailHook
The generated code is actuallyif... else
Nested, we can consider using a recursive function when generating
To enable SyncHook and SyncBailHook’s subclass code factories to pass differentiated result handling, we first split the Create of the HookCodeFactory base class into two parts and split the code assembly logic into a single function:
class HookCodeFactory {
// ...
// omit other identical code
// ...
// create Creates the final call function
create(options) {
this.init(options);
let fn;
// Assemble the header
const header = ` "use strict"; var _x = this._x; `;
// Create a function with the parameters and function body passed in
fn = new Function(this.args(),
header +
this.content()); // Note that the content function is not implemented in the base class HookCodeFactory, but in the subclass
this.deinit();
return fn;
}
// Assemble the function body
// callTapsSeries is not called in the base class, but in the subclass
callTapsSeries() {
const { taps } = this.options;
let code = ' ';
for (let i = 0; i < taps.length; i++) {
code += `
var _fn${i} = _x[${i}];
_fn${i}(The ${this.args()});
`
}
returncode; }}Copy the code
Pay special attention to the code abovecreate
Function is called when the body of the function is generatedthis.content
, butthis.content
Is not implemented in the base class, which requires subclasses to be usedHookCodeFactory
Need to inherit him and realize their owncontent
Delta function, so this delta right herecontent
Function is also an abstract interface. theSyncHook
The code should look like this:
// SyncHook.js
/ /... Omit other code of the same kind...
// SyncHookCodeFactory inherits HookCodeFactory and implements the Content function
class SyncHookCodeFactory extends HookCodeFactory {
content() {
return this.callTapsSeries(); // callTapsSeries is the base class}}// Use SyncHookCodeFactory to create a Factory
const factory = new SyncHookCodeFactory();
const COMPILE = function (options) {
factory.setup(this, options);
return factory.create(options);
};
Copy the code
** Notice here: ** The subclass’s content actually calls the base class’s callTapsSeries to generate the final function body. So the relationship between these functions is actually like this:
So what is the purpose of this design? To allow subclass Content to pass arguments to base class callTapsSeries, generate a different function body. We’ll see that in a moment in SyncBailHook’s code factory.
To generate the body of the SyncBailHook function, we need to make callTapsSeries support an onResult argument, like this:
class HookCodeFactory {
/ /... Omit other same code...
// Assemble the body of the function to support options.onResult
callTapsSeries(options) {
const { taps } = this.options;
let code = ' ';
let i = 0;
const onResult = options && options.onResult;
// Write a next function to start generating the body of the function with the onResult callback
// Next and onResult make recursive calls to each other to generate the final function body
const next = () = > {
if(i >= taps.length) return ' ';
const result = `_result${i}`;
const code = `
var _fn${i} = _x[${i}];
var ${result} = _fn${i}(The ${this.args()});
${onResult(i++, result, next)}
`;
return code;
}
// Support the onResult parameter
if(onResult) {
code = next();
} else {
// When there is no onResult parameter, SyncHook remains the same as before
for(; i< taps.length; i++) {
code += `
var _fn${i} = _x[${i}];
_fn${i}(The ${this.args()});
`}}returncode; }}Copy the code
Then our SyncBailHook code factory needs to pass an onResult argument when it inherits the factory base class, like this:
const Hook = require('./Hook');
const HookCodeFactory = require("./HookCodeFactory");
SyncBailHookCodeFactory inherits HookCodeFactory and implements the Content function
// The custom onResult function is passed in to the content, which calls next recursively to generate nested if... else...
class SyncBailHookCodeFactory extends HookCodeFactory {
content() {
return this.callTapsSeries({
onResult: (i, result, next) = >
`if(${result}! == undefined) {\nreturn${result}; \n} else {\n${next()}}\n`}); }}// Use SyncHookCodeFactory to create a Factory
const factory = new SyncBailHookCodeFactory();
const COMPILE = function (options) {
factory.setup(this, options);
return factory.create(options);
};
function SyncBailHook(args = []) {
// The basic structure is the same as SyncHook
const hook = new Hook(args);
hook.constructor = SyncBailHook;
// Use HookCodeFactory to create the final call function
hook.compile = COMPILE;
return hook;
}
Copy the code
Now run the code, the effect is the same as before, and you are done
Implementation of other hooks
At this point, the source architecture and basic implementation of Tapable have been clarified, but this article only uses SyncHook and SyncBailHook as examples. Others, such as AsyncParallelHook, have not been expanded. AsyncParallelHook (); AsyncParallelHook ();
class AsyncParallelHook {
constructor(args = []) {
this._args = args;
this.taps = [];
}
tapAsync(name, task) {
this.taps.push(task);
}
callAsync(. args) {
// Fetch the last callback first
let finalCallback = args.pop();
// Define an I variable and a done function that checks the I value and queue length on each execution to determine whether callAsync's final callback is executed
let i = 0;
let done = () = > {
if (++i === this.taps.length) { finalCallback(); }};// Execute the event handlers in turn
this.taps.forEach(task= > task(...args, done));
}
}
Copy the code
Then it abstracts its callAsync function into a code factory class and constructs it dynamically using string concatenation. The overall idea is the same as before. Specific implementation process can refer to tapable source code:
Hook type of source
SyncHook class source
SyncBailHook class source
HookCodeFactory class source
conclusion
This article can run the example code has been uploaded to GitHub, we take down while playing while reading the article effect better: github.com/dennis-jian… .
Here is a summary of the idea of this paper:
tapable
A variety ofHook
It’s all publish-subscribe.- The various
Hook
In fact, there is no problem to implement it independently, but because it is a publish and subscribe model, there will be a lot of repetitive code, sotapable
Several abstractions were made. - The first abstraction is to extract one
Hook
Base class, which implements the common parts of initialization and event registration, for eachHook
thecall
It’s all different. You have to do it yourself. - The second abstraction is each
Hook
In realizing their owncall
When, found that the code also has many similarities, so extracted a code factory, used for dynamic generationcall
The function body of. - In general,
tapable
The code is not that difficult, but with two abstractions, the overall code architecture is a bit less readable, and should be much better after this article has combed through it.
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…
The resources
Tapable: juejin.cn/post/693979…
Tapable source address: github.com/webpack/tap…