1 TapableIntroduction to the

Webpack is essentially a mechanism of event flow, and its workflow is to connect various plug-ins in series. The core of implementing all these is Tapable. The core Compiler responsible for Compilation and the Compilation responsible for creating bundles in WebPack are both instances of Tapable. This article focuses on the hook functions in Tapable.

The tapable package exposes a number of hook classes that can be used to create hook functions for plug-ins. These include:

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

All hook constructors take an optional argument, which is an array of string arguments, as follows:

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

Here we will introduce the hook usage in detail, as well as some hook class implementation principle.

An overview of the 2 hooks

Commonly used hooks mainly include the following types, which are divided into synchronous and asynchronous, asynchronous and concurrent execution and serial execution, as shown below:

The serial number The name of the hook Implement way Use the point
1 SyncHook Synchronous serial You don’t care about the return value of the listening function
2 SyncBailHook Synchronous serial As long as one of the listening functions returns a value that is notnull, all the remaining logic is skipped
3 SyncWaterfallHook Synchronous serial The return value of the previous listener can be passed to the next listener
4 SyncLoopHook Synchronous cycle If the listener function returns when firedtrueThe listener is executed repeatedly if the returnundefinedExits the loop
5 AsyncParallelHook Asynchronous concurrent You don’t care about the return value of the listening function
6 AsyncParallelBailHook Asynchronous concurrent As long as the return value of the listening function is notnull, will skip to the following listening functioncallAsyncWait for the callback function that triggers the function binding, and then execute the bound callback
7 AsyncSeriesHook Asynchronous serial port No relationshipcallback()The parameters of the
8 AsyncSeriesBailHook Asynchronous serial port callback()Is notnull“Will be executed directlycallAsyncThe callback function that triggers function binding
9 AsyncSeriesWaterfallHook Asynchronous serial port In the last listening functioncallback(err, data)Which can be used as an argument to the next listening function

3 hooks

Sync * hooks

Synchronous serial

(1) SyncHook

You don’t care about the return value of the listening function

  • usage
const { SyncHook } = require("tapable");
let queue = new SyncHook(['name']); // All constructors take an optional argument, which is an array of strings.

/ / subscribe
queue.tap('1'.function (name, name2) {// The first argument to tap is used to identify the subscribed function
    console.log(name, name2, 1);
    return '1'
});
queue.tap('2'.function (name) {
    console.log(name, 2);
});
queue.tap('3'.function (name) {
    console.log(name, 3);
});

/ / release
queue.call('webpack'.'webpack-cli');// Publish the function that triggers the subscription and pass in parameters

// Execution result:
/* webpack - undefined (); /* webpack - undefined (); /* webpack - undefined (); /* webpack - undefined ()
Copy the code
  • The principle of
class SyncHook_MY{
    constructor() {this.hooks = [];
    }

    / / subscribe
    tap(name, fn){
        this.hooks.push(fn);
    }

    / / release
    call(){
        this.hooks.forEach(hook= > hook(...arguments));
    }
}
Copy the code

(2) SyncBailHook

As long as one of the listening functions does not return null, all the rest of the logic is skipped

  • usage
const {
    SyncBailHook
} = require("tapable");

let queue = new SyncBailHook(['name']); 

queue.tap('1'.function (name) {
    console.log(name, 1);
});
queue.tap('2'.function (name) {
    console.log(name, 2);
    return 'wrong'
});
queue.tap('3'.function (name) {
    console.log(name, 3);
});

queue.call('webpack');

// Execution result:
/* webpack 1 webpack 2 */
Copy the code
  • The principle of
class SyncBailHook_MY {
    constructor() {
        this.hooks = [];
    }

    / / subscribe
    tap(name, fn) {
        this.hooks.push(fn);
    }

    / / release
    call() {
        for (let i = 0, l = this.hooks.length; i < l; i++) {
            let hook = this.hooks[i];
            letresult = hook(... arguments);if (result) {
                break; }}}}Copy the code

(3) SyncWaterfallHook

The return value of the previous listener can be passed to the next listener

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

let queue = new SyncWaterfallHook(['name']); // The return value of the previous function can be passed to the next function queue.tap('1'.function (name) {
    console.log(name, 1);
    return 1;
});
queue.tap('2'.function (data) {
    console.log(data, 2);
    return 2;
});
queue.tap('3'.function (data) {
    console.log(data, 3);
});

queue.call('webpack'); /* webpack 1 1 2 2 3 */Copy the code
  • The principle of
class SyncWaterfallHook_MY{
    constructor() {this.hooks = [];
    }
    
    / / subscribe
    tap(name, fn){
        this.hooks.push(fn);
    }

    / / release
    call(){
        let result = null;
        for(let i = 0, l = this.hooks.length; i < l; i++) {
            let hook = this.hooks[i];
            result = i == 0? hook(... arguments): hook(result); }}}Copy the code

(4) SyncLoopHook

When the listener is fired, the listener is executed repeatedly if it returns true, and if it returns undefined, it exits the loop

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

let queue = new SyncLoopHook(['name']); 

let count = 3;
queue.tap('1'.function (name) {
    console.log('count: ', count--);
    if (count > 0) {
        return true;
    }
    return;
});

queue.call('webpack');

// Execution result:
/* count: 3 count: 2 count: 1 */
Copy the code
  • The principle of
class SyncLoopHook_MY {
    constructor() {
        this.hook = null;
    }

    / / subscribe
    tap(name, fn) {
        this.hook = fn;
    }

    / / release
    call() {
        let result;
        do {
            result = this.hook(... arguments); }while (result)
    }
}
Copy the code

Async * hooks

Asynchronous parallel

(1) AsyncParallelHook

You don’t care about the return value of the listening function.

There are three registration/publishing modes, as follows:

Asynchronous subscription A method is called
tap callAsync
tapAsync callAsync
tapPromise promise
  • usage – tap
const {
    AsyncParallelHook
} = require("tapable");

let queue1 = new AsyncParallelHook(['name']);
console.time('cost');
queue1.tap('1'.function (name) {
    console.log(name, 1);
});
queue1.tap('2'.function (name) {
    console.log(name, 2);
});
queue1.tap('3'.function (name) {
    console.log(name, 3);
});
queue1.callAsync('webpack', err => {
    console.timeEnd('cost');
});

// Execution result
/* webpack 1 webpack 2 webpack 3 cost: 4.520ms */
Copy the code
  • usage – tapAsync
let queue2 = new AsyncParallelHook(['name']);
console.time('cost1');
queue2.tapAsync('1'.function (name, cb) {
    setTimeout((a)= > {
        console.log(name, 1);
        cb();
    }, 1000);
});
queue2.tapAsync('2'.function (name, cb) {
    setTimeout((a)= > {
        console.log(name, 2);
        cb();
    }, 2000);
});
queue2.tapAsync('3'.function (name, cb) {
    setTimeout((a)= > {
        console.log(name, 3);
        cb();
    }, 3000);
});

queue2.callAsync('webpack', () = > {console.log('over');
    console.timeEnd('cost1');
});

// Execution result
Webpack 1 webpack 2 webpack 3 over time: 3004.411ms */
Copy the code
  • usage – promise
let queue3 = new AsyncParallelHook(['name']);
console.time('cost3');
queue3.tapPromise('1'.function (name, cb) {
   return new Promise(function (resolve, reject) {
       setTimeout((a)= > {
           console.log(name, 1);
           resolve();
       }, 1000);
   });
});

queue3.tapPromise('1'.function (name, cb) {
   return new Promise(function (resolve, reject) {
       setTimeout((a)= > {
           console.log(name, 2);
           resolve();
       }, 2000);
   });
});

queue3.tapPromise('1'.function (name, cb) {
   return new Promise(function (resolve, reject) {
       setTimeout((a)= > {
           console.log(name, 3);
           resolve();
       }, 3000);
   });
});

queue3.promise('webpack')
   .then((a)= > {
       console.log('over');
       console.timeEnd('cost3'); = > {}, ()console.log('error');
       console.timeEnd('cost3');
   });
Webpack 1 webpack 2 webpack 3 over cost3: 3007.925ms */
Copy the code

(2) AsyncParallelBailHook

As long as the listener does not return null, the listener will skip to the callback function that triggers the binding, such as callAsync, and then execute the bound callback function.

  • usage – tap
let queue1 = new AsyncParallelBailHook(['name']);
console.time('cost');
queue1.tap('1'.function (name) {
    console.log(name, 1);
});
queue1.tap('2'.function (name) {
    console.log(name, 2);
    return 'wrong'
});
queue1.tap('3'.function (name) {
    console.log(name, 3);
});
queue1.callAsync('webpack', err => {
    console.timeEnd('cost');
});
// Execution result:
/* Webpack 1 webpack 2 cost: 4.975ms */

Copy the code
  • usage – tapAsync
let queue2 = new AsyncParallelBailHook(['name']);
console.time('cost1');
queue2.tapAsync('1'.function (name, cb) {
    setTimeout((a)= > {
        console.log(name, 1);
        cb();
    }, 1000);
});
queue2.tapAsync('2'.function (name, cb) {
    setTimeout((a)= > {
        console.log(name, 2);
        return 'wrong';// The last callback is not called
        cb();
    }, 2000);
});
queue2.tapAsync('3'.function (name, cb) {
    setTimeout((a)= > {
        console.log(name, 3);
        cb();
    }, 3000);
});

queue2.callAsync('webpack', () = > {console.log('over');
    console.timeEnd('cost1');
});

// Execution result:
/* webpack 1 webpack 2 webpack 3 */
Copy the code
  • usage – promise
let queue3 = new AsyncParallelBailHook(['name']);
console.time('cost3');
queue3.tapPromise('1'.function (name, cb) {
    return new Promise(function (resolve, reject) {
        setTimeout((a)= > {
            console.log(name, 1);
            resolve();
        }, 1000);
    });
});

queue3.tapPromise('2'.function (name, cb) {
    return new Promise(function (resolve, reject) {
        setTimeout((a)= > {
            console.log(name, 2);
            reject('wrong');The final callback is not called when the reject() argument is a non-null argument
        }, 2000);
    });
});

queue3.tapPromise('3'.function (name, cb) {
    return new Promise(function (resolve, reject) {
        setTimeout((a)= > {
            console.log(name, 3);
            resolve();
        }, 3000);
    });
});

queue3.promise('webpack')
    .then((a)= > {
        console.log('over');
        console.timeEnd('cost3'); = > {}, ()console.log('error');
        console.timeEnd('cost3');
    });

// Execution result:
/* webpack 1 webpack 2 error cost3: 2009.970ms webpack 3 */
Copy the code

Asynchronous serial port

(1) AsyncSeriesHook

Parameters that do not matter to callback()

  • usage – tap
const {
    AsyncSeriesHook
} = require("tapable");

// tap
let queue1 = new AsyncSeriesHook(['name']);
console.time('cost1');
queue1.tap('1'.function (name) {
    console.log(1);
    return "Wrong";
});
queue1.tap('2'.function (name) {
    console.log(2);
});
queue1.tap('3'.function (name) {
    console.log(3);
});
queue1.callAsync('zfpx', err => {
    console.log(err);
    console.timeEnd('cost1');
});
// Execution result
/* 1 2 3 undefined cost1: 3.933ms */
Copy the code
  • usage – tapAsync
let queue2 = new AsyncSeriesHook(['name']);
console.time('cost2');
queue2.tapAsync('1'.function (name, cb) {
    setTimeout((a)= > {
        console.log(name, 1);
        cb();
    }, 1000);
});
queue2.tapAsync('2'.function (name, cb) {
    setTimeout((a)= > {
        console.log(name, 2);
        cb();
    }, 2000);
});
queue2.tapAsync('3'.function (name, cb) {
    setTimeout((a)= > {
        console.log(name, 3);
        cb();
    }, 3000);
});

queue2.callAsync('webpack', (err) => {
    console.log(err);
    console.log('over');
    console.timeEnd('cost2');
}); 
// Execution result
/* webpack 2 - assumed 'cost2' : 6019.621ms */
Copy the code
  • usage – promise
let queue3 = new AsyncSeriesHook(['name']);
console.time('cost3');
queue3.tapPromise('1'.function(name){
   return new Promise(function(resolve){
       setTimeout(function(){
           console.log(name, 1);
           resolve();
       },1000)}); }); queue3.tapPromise('2'.function(name,callback){
    return new Promise(function(resolve){
        setTimeout(function(){
            console.log(name, 2);
            resolve();
        },2000)}); }); queue3.tapPromise('3'.function(name,callback){
    return new Promise(function(resolve){
        setTimeout(function(){
            console.log(name, 3);
            resolve();
        },3000)}); }); queue3.promise('webapck').then(err= >{
    console.log(err);
    console.timeEnd('cost3');
});

// Execution result
/* webapck 1 webapck 2 webapck 3 undefined cost3: 6021.817ms */
Copy the code
  • The principle of
class AsyncSeriesHook_MY {
    constructor() {
        this.hooks = [];
    }

    tapAsync(name, fn) {
        this.hooks.push(fn);
    }

    callAsync() {
        var slef = this;
        var args = Array.from(arguments);
        let done = args.pop();
        let idx = 0;

        function next(err) {
            // If the next argument has a value, jump directly to the callback function that executes callAsync
            if (err) return done(err);
            let fn = slef.hooks[idx++];
            fn ? fn(...args, next) : done();
        }
        next();
    }
}
Copy the code

(2) AsyncSeriesBailHook

If the argument to callback() is not null, the function that triggers the binding, such as callAsync, is executed directly

  • usage – tap
const {
    AsyncSeriesBailHook
} = require("tapable");

// tap
let queue1 = new AsyncSeriesBailHook(['name']);
console.time('cost1');
queue1.tap('1'.function (name) {
    console.log(1);
    return "Wrong";
});
queue1.tap('2'.function (name) {
    console.log(2);
});
queue1.tap('3'.function (name) {
    console.log(3);
});
queue1.callAsync('webpack', err => {
    console.log(err);
    console.timeEnd('cost1');
});

// Execution result:
/* 1 NULL cost1: 3.979ms */
Copy the code
  • usage – tapAsync
let queue2 = new AsyncSeriesBailHook(['name']);
console.time('cost2');
queue2.tapAsync('1'.function (name, callback) {
    setTimeout(function () {
        console.log(name, 1);
        callback();
    }, 1000)}); queue2.tapAsync('2'.function (name, callback) {
    setTimeout(function () {
        console.log(name, 2);
        callback('wrong');
    }, 2000)}); queue2.tapAsync('3'.function (name, callback) {
    setTimeout(function () {
        console.log(name, 3);
        callback();
    }, 3000)}); queue2.callAsync('webpack', err => {
    console.log(err);
    console.log('over');
    console.timeEnd('cost2');
});
// Execution result

/* webpack 1 webpack 2 wrong over cost2: 3014.616ms */
Copy the code
  • usage – promise
let queue3 = new AsyncSeriesBailHook(['name']);
console.time('cost3');
queue3.tapPromise('1'.function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(name, 1);
            resolve();
        }, 1000)}); }); queue3.tapPromise('2'.function (name, callback) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(name, 2);
            reject();
        }, 2000)}); }); queue3.tapPromise('3'.function (name, callback) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            console.log(name, 3);
            resolve();
        }, 3000)}); }); queue3.promise('webpack').then(err= > {
    console.log(err);
    console.log('over');
    console.timeEnd('cost3');
}, err => {
    console.log(err);
    console.log('error');
    console.timeEnd('cost3');
});
// Execution result:
/* webpack 1 - undefined error cost3: 3017.608ms */
Copy the code

(3) AsyncSeriesWaterfallHook

The second argument of callback(err, data) in the previous listener can be used as an argument to the next listener

  • usage – tap
const {
    AsyncSeriesWaterfallHook
} = require("tapable");

// tap
let queue1 = new AsyncSeriesWaterfallHook(['name']);
console.time('cost1');
queue1.tap('1'.function (name) {
    console.log(name, 1);
    return 'lily'
});
queue1.tap('2'.function (data) {
    console.log(2, data);
    return 'Tom';
});
queue1.tap('3'.function (data) {
    console.log(3, data);
});
queue1.callAsync('webpack', err => {
    console.log(err);
    console.log('over');
    console.timeEnd('cost1'); }); /* webpack 1 2'lily'
3 'Tom'Null over cost1: 5.525ms */Copy the code
  • usage – tapAsync
let queue2 = new AsyncSeriesWaterfallHook(['name']);
console.time('cost2');
queue2.tapAsync('1'.function (name, callback) {
    setTimeout(function () {
        console.log('1', name);
        callback(null.2);
    }, 1000)}); queue2.tapAsync('2'.function (data, callback) {
    setTimeout(function () {
        console.log('2.', data);
        callback(null.3);
    }, 2000)}); queue2.tapAsync('3'.function (data, callback) {
    setTimeout(function () {
        console.log('3:, data);
        callback(null.3);
    }, 3000)}); queue2.callAsync('webpack', err => {
    console.log(err);
    console.log('over');
    console.timeEnd('cost2');
});
// Execution result:
/* 1: webpack 2: 2 3:3 null over cost2: 6016.889ms */
Copy the code
  • usage – promise
let queue3 = new AsyncSeriesWaterfallHook(['name']);
console.time('cost3');
queue3.tapPromise('1'.function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log('1', name);
            resolve('1');
        }, 1000)}); }); queue3.tapPromise('2'.function (data, callback) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            console.log('2:', data);
            resolve('2');
        }, 2000)}); }); queue3.tapPromise('3'.function (data, callback) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            console.log('3:, data);
            resolve('over');
        }, 3000)}); }); queue3.promise('webpack').then(err= > {
    console.log(err);
    console.timeEnd('cost3');
}, err => {
    console.log(err);
    console.timeEnd('cost3');
});
// Execution result:
/* 1: webpack 2: 1 3:2 over cost3: 6016.703ms */
Copy the code
  • The principle of
class AsyncSeriesWaterfallHook_MY {
    constructor() {
        this.hooks = [];
    }

    tapAsync(name, fn) {
        this.hooks.push(fn);
    }

    callAsync() {
        let self = this;
        var args = Array.from(arguments);

        let done = args.pop();
        console.log(args);
        let idx = 0;
        let result = null;

        function next(err, data) {
            if (idx >= self.hooks.length) return done();
            if (err) {
                return done(err);
            }
            let fn = self.hooks[idx++];
            if (idx == 1) { fn(... args, next); }else{ fn(data, next); } } next(); }}Copy the code