webpack4.0源碼分析之Tapable

1 Tapable簡介

webpack本質上是一種事件流的機制,它的工做流程就是將各個插件串聯起來,而實現這一切的核心就是Tapablewebpack中最核心的負責編譯的Compiler和負責建立bundles的Compilation都是Tapable的實例。本文主要介紹一下Tapable中的鉤子函數。javascript

tapable包暴露出不少鉤子類,這些類能夠用來爲插件建立鉤子函數,主要包含如下幾種:java

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = require("tapable");
複製代碼

全部鉤子類的構造函數都接收一個可選的參數,這個參數是一個由字符串參數組成的數組,以下:webpack

const hook = new SyncHook(["arg1", "arg2", "arg3"]);
複製代碼

下面咱們就詳細介紹一下鉤子的用法,以及一些鉤子類實現的原理。web

2 hooks概覽

經常使用的鉤子主要包含如下幾種,分爲同步和異步,異步又分爲併發執行和串行執行,以下圖: 數組

hooks
首先,總體感覺下鉤子的用法,以下

序號 鉤子名稱 執行方式 使用要點
1 SyncHook 同步串行 不關心監聽函數的返回值
2 SyncBailHook 同步串行 只要監聽函數中有一個函數的返回值不爲 null,則跳過剩下全部的邏輯
3 SyncWaterfallHook 同步串行 上一個監聽函數的返回值能夠傳給下一個監聽函數
4 SyncLoopHook 同步循環 當監聽函數被觸發的時候,若是該監聽函數返回true時則這個監聽函數會反覆執行,若是返回 undefined 則表示退出循環
5 AsyncParallelHook 異步併發 不關心監聽函數的返回值
6 AsyncParallelBailHook 異步併發 只要監聽函數的返回值不爲 null,就會忽略後面的監聽函數執行,直接跳躍到callAsync等觸發函數綁定的回調函數,而後執行這個被綁定的回調函數
7 AsyncSeriesHook 異步串行 不關係callback()的參數
8 AsyncSeriesBailHook 異步串行 callback()的參數不爲null,就會直接執行callAsync等觸發函數綁定的回調函數
9 AsyncSeriesWaterfallHook 異步串行 上一個監聽函數的中的callback(err, data)的第二個參數,能夠做爲下一個監聽函數的參數

3 鉤子

sync* 鉤子

同步串行

(1) SyncHook

不關心監聽函數的返回值promise

  • usage
const { SyncHook } = require("tapable");
let queue = new SyncHook(['name']); //全部的構造函數都接收一個可選的參數,這個參數是一個字符串的數組。

// 訂閱
queue.tap('1', function (name, name2) {// tap 的第一個參數是用來標識訂閱的函數的
    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);
});

// 發佈
queue.call('webpack', 'webpack-cli');// 發佈的時候觸發訂閱的函數 同時傳入參數

// 執行結果:
/* webpack undefined 1 // 傳入的參數須要和new實例的時候保持一致,不然獲取不到多傳的參數 webpack 2 webpack 3 */
複製代碼
  • 原理
class SyncHook_MY{
    constructor(){
        this.hooks = [];
    }

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

    // 發佈
    call(){
        this.hooks.forEach(hook => hook(...arguments));
    }
}
複製代碼

(2) SyncBailHook

只要監聽函數中有一個函數的返回值不爲 null,則跳過剩下全部的邏輯bash

  • 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');

// 執行結果:
/* webpack 1 webpack 2 */
複製代碼
  • 原理
class SyncBailHook_MY {
    constructor() {
        this.hooks = [];
    }

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

    // 發佈
    call() {
        for (let i = 0, l = this.hooks.length; i < l; i++) {
            let hook = this.hooks[i];
            let result = hook(...arguments);
            if (result) {
                break;
            }
        }
    }
}
複製代碼

(3) SyncWaterfallHook

上一個監聽函數的返回值能夠傳給下一個監聽函數併發

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

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

// 上一個函數的返回值能夠傳給下一個函數
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
*/
複製代碼
  • 原理
class SyncWaterfallHook_MY{
    constructor(){
        this.hooks = [];
    }
    
    // 訂閱
    tap(name, fn){
        this.hooks.push(fn);
    }

    // 發佈
    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); 
        }
    }
}
複製代碼

(4) SyncLoopHook

當監聽函數被觸發的時候,若是該監聽函數返回true時則這個監聽函數會反覆執行,若是返回 undefined 則表示退出循環異步

  • 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');

// 執行結果:
/* count: 3 count: 2 count: 1 */
複製代碼
  • 原理
class SyncLoopHook_MY {
    constructor() {
        this.hook = null;
    }

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

    // 發佈
    call() {
        let result;
        do {
            result = this.hook(...arguments);
        } while (result)
    }
}
複製代碼

async* 鉤子

異步並行

(1) AsyncParallelHook

不關心監聽函數的返回值。async

有三種註冊/發佈的模式,以下:

異步訂閱 調用方法
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');
});

// 執行結果
/* webpack 1 webpack 2 webpack 3 cost: 4.520ms */
複製代碼
  • usage - tapAsync
let queue2 = new AsyncParallelHook(['name']);
console.time('cost1');
queue2.tapAsync('1', function (name, cb) {
    setTimeout(() => {
        console.log(name, 1);
        cb();
    }, 1000);
});
queue2.tapAsync('2', function (name, cb) {
    setTimeout(() => {
        console.log(name, 2);
        cb();
    }, 2000);
});
queue2.tapAsync('3', function (name, cb) {
    setTimeout(() => {
        console.log(name, 3);
        cb();
    }, 3000);
});

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

// 執行結果
/* webpack 1 webpack 2 webpack 3 over time: 3004.411ms */
複製代碼
  • usage - promise
let queue3 = new AsyncParallelHook(['name']);
console.time('cost3');
queue3.tapPromise('1', function (name, cb) {
   return new Promise(function (resolve, reject) {
       setTimeout(() => {
           console.log(name, 1);
           resolve();
       }, 1000);
   });
});

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

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

queue3.promise('webpack')
   .then(() => {
       console.log('over');
       console.timeEnd('cost3');
   }, () => {
       console.log('error');
       console.timeEnd('cost3');
   });
/* webpack 1 webpack 2 webpack 3 over cost3: 3007.925ms */
複製代碼

(2) AsyncParallelBailHook

只要監聽函數的返回值不爲 null,就會忽略後面的監聽函數執行,直接跳躍到callAsync等觸發函數綁定的回調函數,而後執行這個被綁定的回調函數。

  • 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');
});
// 執行結果:
/* webpack 1 webpack 2 cost: 4.975ms */

複製代碼
  • usage - tapAsync
let queue2 = new AsyncParallelBailHook(['name']);
console.time('cost1');
queue2.tapAsync('1', function (name, cb) {
    setTimeout(() => {
        console.log(name, 1);
        cb();
    }, 1000);
});
queue2.tapAsync('2', function (name, cb) {
    setTimeout(() => {
        console.log(name, 2);
        return 'wrong';// 最後的回調就不會調用了
        cb();
    }, 2000);
});
queue2.tapAsync('3', function (name, cb) {
    setTimeout(() => {
        console.log(name, 3);
        cb();
    }, 3000);
});

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

// 執行結果:
/* webpack 1 webpack 2 webpack 3 */
複製代碼
  • usage - promise
let queue3 = new AsyncParallelBailHook(['name']);
console.time('cost3');
queue3.tapPromise('1', function (name, cb) {
    return new Promise(function (resolve, reject) {
        setTimeout(() => {
            console.log(name, 1);
            resolve();
        }, 1000);
    });
});

queue3.tapPromise('2', function (name, cb) {
    return new Promise(function (resolve, reject) {
        setTimeout(() => {
            console.log(name, 2);
            reject('wrong');// reject()的參數是一個不爲null的參數時,最後的回調就不會再調用了
        }, 2000);
    });
});

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

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

// 執行結果:
/* webpack 1 webpack 2 error cost3: 2009.970ms webpack 3 */
複製代碼

異步串行

(1) AsyncSeriesHook

不關係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');
});
// 執行結果
/* 1 2 3 undefined cost1: 3.933ms */
複製代碼
  • usage - tapAsync
let queue2 = new AsyncSeriesHook(['name']);
console.time('cost2');
queue2.tapAsync('1', function (name, cb) {
    setTimeout(() => {
        console.log(name, 1);
        cb();
    }, 1000);
});
queue2.tapAsync('2', function (name, cb) {
    setTimeout(() => {
        console.log(name, 2);
        cb();
    }, 2000);
});
queue2.tapAsync('3', function (name, cb) {
    setTimeout(() => {
        console.log(name, 3);
        cb();
    }, 3000);
});

queue2.callAsync('webpack', (err) => {
    console.log(err);
    console.log('over');
    console.timeEnd('cost2');
}); 
// 執行結果
/* webpack 1 webpack 2 webpack 3 undefined over cost2: 6019.621ms */
複製代碼
  • 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');
});

// 執行結果
/* webapck 1 webapck 2 webapck 3 undefined cost3: 6021.817ms */
複製代碼
  • 原理
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) {
            // 若是next的參數有值,就直接跳躍到 執行callAsync的回調函數
            if (err) return done(err);
            let fn = slef.hooks[idx++];
            fn ? fn(...args, next) : done();
        }
        next();
    }
}
複製代碼

(2) AsyncSeriesBailHook

callback()的參數不爲null,就會直接執行callAsync等觸發函數綁定的回調函數

  • 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');
});

// 執行結果:
/* 1 null cost1: 3.979ms */
複製代碼
  • 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');
});
// 執行結果

/* webpack 1 webpack 2 wrong over cost2: 3014.616ms */
複製代碼
  • 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');
});
// 執行結果:
/* webpack 1 webpack 2 undefined error cost3: 3017.608ms */
複製代碼

(3) AsyncSeriesWaterfallHook

上一個監聽函數的中的callback(err, data)的第二個參數,能夠做爲下一個監聽函數的參數

  • 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
*/
複製代碼
  • 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');
});
// 執行結果:
/* 1: webpack 2: 2 3: 3 null over cost2: 6016.889ms */
複製代碼
  • 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');
});
// 執行結果:
/* 1: webpack 2: 1 3: 2 over cost3: 6016.703ms */
複製代碼
  • 原理
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();
    }
}
複製代碼
相關文章
相關標籤/搜索