I haven’t updated the article these days, so I spent all my time on the core principle analysis of Tabable. Now I finally understand tabable and share it with you through this article. Finally, I found a very easy way to master Tabable.
After several days’ study, I summed up Tapable into a sentence “monitor specific events through a mechanism”. Take the most classic example of audio player, there is only one core player service, and there are many objects that need to listen to play events, such as Mini player and main player, both of which need to listen to audio play progress, play start, play pause events. Both players in the image below listen for playback progress:
Faced with this demand, most people adopt the “publish and subscribe mode”. Tapable can also solve the similar business of event monitoring, but it is more flexible and can do more business, such as data transfer between hook functions, asynchronous hook, hook stream processing. This was the first time I learned about the programming ideas of asyncHook, syncHook, syncBailHook and syncWaterfallHook. Of course, Tapable ultimately serves Webpack, and many scenarios are designed with WebPack in mind.
1. Overall Hook overview
Tapable’s core classes are as follows, mainly divided into synchronous hook and asynchronous hook:
2. Listening and triggering events
There are three ways to add an event listener. If you want to listen for an event, you can add it by:
2.1. Tap (options, fn) : Add synchronization listener function
Options can be objects or strings, and fn is the synchronization callback function. Can be used with all types of hooks, including sync and async.
hook.tap('SuyanSyncHook', (name, age) => {
console.log(`syncHook name: ${name}, age: ${age}`);
});
Copy the code
Trigger the event with the call method: synchoke. call(‘suyan’, 20);
TapAsync (options, fn) : Add async listener function
The last argument to the listener is a callback function. This function cannot be used for sync hooks.
asyncHook.tapAsync('SuyanasyncSeriesHook', (source, callback) => {
setTimeout((a)= > {
console.log(`source3: ${source}`);
callback();
}, 2000);
});
Copy the code
Trigger events with callAsync:
asyncSeriesHook.callAsync('Follow the public account Suyan', ret => {
console.log(`ret = ${ret}`);
});
Copy the code
2.3 tapPromise(options, FN) : the way of promise;
You need to return a Promise object; Cannot be used for sync-type hooks.
asynchook.tapPromise('SuyanasyncParallelHook', (source) => {
return new Promise((resolve, reject) = > {
setTimeout((a)= > {
console.log(`source2: ${source}`);
resolve(`${source}, yan `);
}, 1000);
});
});
Copy the code
Events are raised through promises:
asyncHook.promise('Follow the public account Suyan').then(ret= > {
console.log(`ret = ${ret}`);
});
Copy the code
3. Various Hook implementation analysis
Let’s introduce the functions of various hooks. There are two types of hooks in total. The first type is synchronous Hook.
3.1, SyncHook
Synchronous Hook, Hook events do not interfere with each other. Here is an example of using SyncHook. Two callbacks are added to SyncHook. When the call function is called, the callbacks previously added via TAP will be executed:
const SyncHook = require('./lib/SyncHook');
// Create a SyncHook, followed by a list of parameters
const syncHook = new SyncHook(['name'.'age']);
// Add a hook event
syncHook.tap('SuyanSyncHook', (name, age) => {
console.log(`syncHook name: ${name}, age: ${age}`);
});
// Add a hook event
syncHook.tap('SuyanSyncHook', (name, age) => {
console.log(`1syncHook name: ${name}, age: ${age}`);
});
/ / call hook
syncHook.call('suyan'.20);
Copy the code
How does this work? We analyze the core principle of Tapable through the source code and understand one of the principles. Other hooks can be understood in the same way.
All hooks inherit from the hook class. In my opinion, it mainly does three things:
Provide call functions call, promise, callAsync; Save the execution context of the function, such as function parameters, listeners; Interceptor processing; Let’s look at its constructor
constructor(args) {
// An array to protect the hook arguments
if (!Array.isArray(args)) args = [];
/ / parameters
this._args = args;
// Listener, or subscriber
this.taps = [];
/ / the interceptor
this.interceptors = [];
// These functions ultimately refer to the Object prototype
// Plain call
this.call = this._call;
/ / promise calls
this.promise = this._promise;
// Asynchronous invocation
this.callAsync = this._callAsync;
// All callback functions
this._x = undefined;
}
Copy the code
HookCodeFactory generates functions such as the SyncHook code above, resulting in the following code:
"use strict";
var _context;
// _x holds all callbacks to tap
// syncHook.tap('SuyanSyncHook', FN);
var _x = this._x;
// Execute the first callback function
var _fn0 = _x[0];
_fn0(name, age);
// Execute the second callback function
var _fn1 = _x[1];
_fn1(name, age);
Copy the code
At the heart of Tapable is the dynamically generated Function. You can define functions directly in JavaScript, or you can generate a Function using new Function. The following functions are equivalent:
// Define the function directly
function sum(a, b) {
return a + b;
}
// Generate the Function with new Function
const sum = new Function(['a'.'b'].'return a + b; ');
Copy the code
This is the essence of a Hook, and you can understand what each Hook does through the resulting code.
In short, the core principle of Tapable is to collect all the information about the Function when it is to be executed. According to the information, TAPS, Intercepts, ARgs and Type, tapable generates the Function to be called through new Function. When the listener needs to be notified, the generated Function can be executed directly.
3.2, SyncBailHook
A Hook can be understood by its name, such as SyncHook, which is “pure”, basic Hook. SyncBailHook, SyncWaterfallHook, and SyncLoopHook add modifiers that are related to the return value of the listener function.
One Hook that does not return undefined will end the Hook. Since the second listener returns “learn Webpack,” the third listener will not be executed.
const SyncBailHook = require('./lib/SyncBailHook');
// One hook after another, as long as there is a return undefined end
const syncBailHook = new SyncBailHook(['source']);
syncBailHook.tap('SuyansyncBailHook', (source) => {
console.log(`source1: ${source}`);
});
syncBailHook.tap('SuyansyncBailHook', source => {
console.log(`source2: ${source}`);
return 'learning webpack';
});
syncBailHook.tap('SuyansyncBailHook', source => {
console.log(`source3: ${source}`);
});
let ret = syncBailHook.call('Follow the public account Suyan,');
console.log(`ret = ${ret}`);
Copy the code
Let’s look at the compiled code:
"use strict";
var _context;
var _x = this._x;
console.log('this._x = : '.this._x);
var _fn0 = _x[0];
var _result0 = _fn0(source);
if(_result0 ! = =undefined) {
return _result0;
} else {
var _fn1 = _x[1];
var _result1 = _fn1(source);
if(_result1 ! = =undefined) {
return _result1;
} else {
var _fn2 = _x[2];
var _result2 = _fn2(source);
if(_result2 ! = =undefined) {
return _result2;
} else{}}}Copy the code
3.3, SyncWaterfallHook
In waterfall, the return value of the listener function is passed to the next listener, just like water flowing continuously. The return value of each listener function is passed to the next callback function, and the final return value of the following demo is “follow the public account Suyan, learn webpack with Suyan” :
const SyncWaterfallHook = require('./lib/SyncWaterfallHook');
// Hook after hook, the return value of the previous function is the argument of the next function
const syncWaterfallHook = new SyncWaterfallHook(['source']);
syncWaterfallHook.tap('SuyanSyncWaterfallHook', (source) => {
console.log(`source1: ${source}`);
return `${source}And `;
});
syncWaterfallHook.tap('SuyanSyncWaterfallHook', source => {
console.log(`source2: ${source}`);
return `${source}Plain yan `;;
});
syncWaterfallHook.tap('SuyanSyncWaterfallHook', source => {
console.log(`source3: ${source}`);
return `${source}Learn webpack together;;
});
let ret = syncWaterfallHook.call('Follow the public account Suyan,');
console.log(`ret = ${ret}`);
Copy the code
The final compiled code is as follows:
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(source);
if(_result0 ! = =undefined) {
source = _result0;
}
var _fn1 = _x[1];
var _result1 = _fn1(source);
if(_result1 ! = =undefined) {
source = _result1;
}
var _fn2 = _x[2];
var _result2 = _fn2(source);
if(_result2 ! = =undefined) {
source = _result2;
}
return source;
Copy the code
3.4, SyncLoopHook
Loop, the listener will always be called as long as the return value is not “false”. Do not explain in detail, you can learn by yourself.
The second type is asynchronous hooks, which are usually used for time-consuming operations such as network requests.
3.5 AsyncSeriesHook
For asynchronous hooks, you can add asynchronous functions using tapAsync and tapPromise. The last argument to a listener is callback. If a callback is called with an error argument, the entire chain of listener functions will be interrupted, such as callback(‘err’). When such a callback is encountered, the next listener function will not be executed. Here’s an example:
const AsyncSeriesHook = require('./lib/AsyncSeriesHook');
// asynchronous serial, that is, one by one
const asyncSeriesHook = new AsyncSeriesHook(['source']);
asyncSeriesHook.tapAsync('SuyanasyncSeriesHook', (source, callback) => {
setTimeout((a)= > {
console.log(`source1: ${source}`);
callback();
}, 3000);
});
asyncSeriesHook.tapAsync('SuyanasyncSeriesHook', (source, callback) => {
setTimeout((a)= > {
console.log(`source2: ${source}`);
callback();
}, 1000);
});
asyncSeriesHook.callAsync('Follow the public account Suyan', ret => {
console.log(`ret = ${ret}`);
});
Copy the code
The final compiled code clearly shows what the hook does:
"use strict";
var _context;
var _x = this._x;
// The next function to execute
function _next0() {
var _fn1 = _x[1];
_fn1(source, _err1 => {
if (_err1) {
_callback(_err1);
} else{ _callback(); }}); }// The first function to execute
var _fn0 = _x[0];
_fn0(source, _err0 => {
if (_err0) {
_callback(_err0);
} else{ _next0(); }});Copy the code
3.6, AsyncSeriesBailHook
This Hook is similar to SyncBailHook, except that the callback function is asynchronous. The callback function takes two arguments, the first one being error and the second being result. The entire chain of listener functions will break. Look at the following example:
const AsyncSeriesBailHook = require('./lib/AsyncSeriesBailHook');
// asynchronous serial, that is, one by one
const asyncSeriesBailHook = new AsyncSeriesBailHook(['source']);
asyncSeriesBailHook.tapAsync('SuyanasyncSeriesBailHook', (source, callback) => {
setTimeout((a)= > {
console.log(`source1: ${source}`);
callback();
}, 3000);
});
asyncSeriesBailHook.tapAsync('SuyanasyncSeriesBailHook', (source, callback) => {
setTimeout((a)= > {
console.log(`source2: ${source}`);
callback();
}, 1000);
});
asyncSeriesBailHook.callAsync('Follow the public account Suyan', ret => {
console.log(`ret = ${ret}`);
});
Copy the code
The resulting code is similar to AsyncSeriesHook, except that the result argument is added to the callback:
"use strict";
var _context;
var _x = this._x;
console.log('this._x = : '.this._x);
function _next0() {
var _fn1 = _x[1];
_fn1(source, (_err1, _result1) => {
if (_err1) {
_callback(_err1);
} else {
if(_result1 ! = =undefined) {
_callback(null, _result1);
;
} else{ _callback(); }}}); }var _fn0 = _x[0];
_fn0(source, (_err0, _result0) => {
if (_err0) {
_callback(_err0);
} else {
if(_result0 ! = =undefined) {
_callback(null, _result0);
;
} else{ _next0(); }}});Copy the code
3.7, AsyncSeriesWaterfallHook
Similar to SyncWaterfallHook, the result of the previous callback is passed to the next listener.
3.8, AsyncSeriesLoopHook
Async loop Hook: as long as the callback does not return undefined, the loop will continue to execute until the value of the callback is undefined.
3.9, AsyncParalleHook
Asynchronous parallel hooks, that is, all callbacks are executed simultaneously.
3.10, AsyncParalleBailHook
Asynchronous parallel fuse hook.
Write in the last
Tapable is not friendly for beginners and has a learning cost. Generally speaking, tapable is mainly divided into synchronous hook and asynchronous hook, and each type of hook is divided into Bail (when the function has any return value, the following hook callback function will terminate), waterfall (waterfall flow, The return value of the function will be passed to the next callback function), loop (loop, as long as the function return value is not undefined, will continue). If you want to thoroughly understand the various types of hooks, you can easily understand them by analyzing the resulting function code.
This section belongs to theoretical knowledge. In the next section, we combined compiler to deepen our understanding of tapable.
Webpack series of articles
- Day 1: Stage 9: Everyone on the front end needs to understand and understand the front-end build
- Day 2: What exactly is webpack, beginner’s head spinning
- Day 3: Install Webpack and get Vue packing done
- Day 4: From using Loader to implementing Loader · Webpack
- Day 5: The Webpack plugin was easy to fix
- Day 6: Generate HTML dynamically with Webpack
- Day 7: WebPack AIDS in efficient development
- Day 8: Webpack meets Tapable to implement plugin system
- Day 9: Understand the spirit of Webpack tapable
- Day 10: Unified management of multi-project WebPack profiles