webpack4.0原始碼分析之Tapable

whynotgonow發表於2018-04-06

1 Tapable簡介

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

tapable包暴露出很多鉤子類,這些類可以用來為外掛建立鉤子函式,主要包含以下幾種:

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

所有鉤子類的建構函式都接收一個可選的引數,這個引數是一個由字串引數組成的陣列,如下:

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

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

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

不關心監聽函式的返回值

  • 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,則跳過剩下所有的邏輯

  • 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

不關心監聽函式的返回值。

有三種註冊/釋出的模式,如下:

非同步訂閱 呼叫方法
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();
    }
}
複製程式碼

相關文章