1 Tapable
簡介
webpack
本質上是一種事件流的機制,它的工作流程就是將各個外掛串聯起來,而實現這一切的核心就是Tapable
,webpack
中最核心的負責編譯的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概覽
常用的鉤子主要包含以下幾種,分為同步和非同步,非同步又分為併發執行和序列執行,如下圖:
首先,整體感受下鉤子的用法,如下序號 | 鉤子名稱 | 執行方式 | 使用要點 |
---|---|---|---|
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();
}
}
複製程式碼