前言
為什麼我們要學tapable,因為....webpack原始碼裡面都是用的tapable來實現鉤子掛載的,作為一個有點追求的code,webpack怎麼能只滿足於用呢?當然是要去看原始碼,寫loader,plugin啦.在這之前,要是不清楚tapable的用法,原始碼那是更不用看了,看不懂.....所以,今天來講一下tapable吧
1. tapable
webpack本質上是一種事件流的機制,他的工作流程就是將各個外掛串聯起來,而實現這一切的核心就是Tapable,webpack中最核心的負責編譯的Compiler和負責建立的bundles的Compilation都是Tapable的例項
tapable建立例項時傳遞的引數對於程式執行並沒有任何作用,只是給原始碼閱讀者提供幫助
同樣的,在使用tap*註冊監聽時,傳遞的第一個引數,也只是一個標識,並不會在程式執行中產生任何影響。而第二個引數則是回撥函式
2.tapable的用法
const {
SyncHook,
SyncBailHook,
SyncWaterHook,
SyncLoopHook
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
複製程式碼
序號 | 鉤子名稱 | 執行方式 | 使用要點 |
---|---|---|---|
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*型別的鉤子
- 註冊在該鉤子下面的外掛的執行順序都是順序執行
- 只能使用tap註冊,不能使用tapPromise和tapAsync註冊
3.1 SyncHook
序列同步執行,不關心返回值 在SyncHook的例項上註冊了tap之後,只要例項呼叫了call方法,那麼這些tap的回掉函式一定會順序執行一遍
let queue = new SyncHook(['沒任何作用的引數']);
queue.tap(1,(name,age)=>{
console.log(name,age)
})
queue.tap(2,(name,age)=>{
console.log(name,age)
})
queue.tap(3,(name,age)=>{
console.log(name,age)
})
queue.call('bearbao',8)
// 輸出結果
// 'bearbao' 8
// 'bearbao' 8
// 'bearbao' 8
複製程式碼
3.1.1 SyncHook實現
class SyncHook {
constructor(){
this.listeners = [];
}
tap(formal,listener){
this.listeners.push(listener)
}
call(...args){
this.listeners.forEach(l=>l(...args))
}
}
複製程式碼
3.2 SyncBailHook
序列同步執行,有一個返回值不為null則跳過剩下的邏輯
let queue = new SyncBailHook(['name'])
queue.tap(1,name=>{
console.log(name)
})
queue.tap(1,name=>{
console.log(name)
return '1'
})
queue.tap(1,name=>{
console.log(name)
})
queue.call('bearbao')
// 輸出結果,只執行前面兩個回撥,第三個不執行
// bearbao
// bearbao
複製程式碼
實現
class SyncBailHook {
constructor(){
this.listeners = [];
}
tap(formal,listener){
this.listeners.push(listener)
}
call(...args){
for(let i=0;i<this.listeners.length;i++){
if(this.listeners[i]()) break;
}
}
}
複製程式碼
3.3 SyncWaterHook
序列同步執行,第一個註冊的回撥函式會接收call傳進來的所有引數,之後的每個回撥函式只接收到一個引數,就是上一個回撥函式的返回值.
let queue = new SyncWaterHook(['name','age']);
queue.tap(1,(name,age)=>{
console.log(name,age)
return 1
})
queue.tap(2,(ret)=>{
console.log(ret)
return 2
})
queue.tap(3,(ret)=>{
console.log(ret)
return 3
})
queue.call('bearbao', 3)
// 輸出結果
// bearbao 3
// 1
// 2
複製程式碼
SyncWaterHook 實現. SyncWaterHook這個方法很像redux中的compose方法,都是將一個函式的返回值作為引數傳遞給下一個函式.
對下面實現的call方法如果有疑惑,看不大懂的同學可以移步我之前對於compose函式的解讀,裡面有詳細的介紹,這裡就不多加贅述了
class SyncWaterHook{
constructor(){
this.listeners = [];
}
tap(formal,listener){
this.listener.unshift(listener);
}
call(...args){
this.listeners.reduce((a,b)=>(...args)=>a(b(...args)))(...args)
}
}
複製程式碼
3.4 SyncLoopHook
序列同步執行, 監聽函式返回true表示繼續迴圈,返回undefined表示迴圈結束
let queue = new SyncLoopHook;
let index = 0;
queue.tap(1,_=>{
index++
if(index<3){
console.log(index);
return true
}
})
queue.call();
// 輸出結果
// 1
// 2
複製程式碼
SyncLoopHook實現
class SyncLoopHook{
constructor() {
this.tasks=[];
}
tap(name,task) {
this.tasks.push(task);
}
call(...args) {
this.tasks.forEach(task => {
let ret=true;
do {
ret = task(...args);
}while(ret)
});
}
}
複製程式碼
4. Async*型別的鉤子
- 支援tap、tapPromise、tapAsync註冊
- 每次都是呼叫tap、tapSync、tapPromise註冊不同型別的外掛鉤子,通過呼叫call、callAsync 、promise方式呼叫。其實呼叫的時候為了按照一定的執行策略執行,呼叫compile方法快速編譯出一個方法來執行這些外掛。
4.1 AsyncParallel
非同步並行執行
4.1.1 AsyncParallelHook
不關心監聽函式的返回值.
有三種註冊/釋出的模式,如下
非同步訂閱 | 呼叫方法 |
---|---|
tap | callAsync |
tapAsync | callAsync |
tapPromise | promise |
- 通過tap來使用
觸發函式的引數,出了最後一個引數是非同步監聽回撥函式執行完成之後的回撥,其他的引數都是傳遞給回撥函式的引數
let queue = new AsyncParallelHook(['name']);
console.time('cost');
queue.tap('1',function(name){
console.log(name,1);
});
queue.tap('2',function(name){
console.log(name,2);
});
queue.tap('3',function(name){
console.log(name,3);
});
queue.callAsync('bearbao',err=>{
console.log(err);
console.timeEnd('cost');
});
// 執行結果
/*
bearbao 1
bearbao 2
bearbao 3
cost: 4.720ms
*/
複製程式碼
實現
class AsyncParallelHook {
constructor(){
this.listeners = [];
}
tap(name,listener){
this.listeners.push(listener);
}
callAsync(){
this.listeners.forEach(listener=>listener(...arguments));
Array.from(arguments).pop()();
}
}
複製程式碼
- 通過tapAsync來註冊
注意,這裡有個特殊的地方,如何確認某個回撥執行完了呢?,每個監聽回撥的最後一個引數是一個回撥函式,當執行callback之後,會認為當前函式執行完畢
let queue = new AsyncParallelHook(['name']);
console.time('cost');
queue.tapAsync('1',function(name,callback){
setTimeout(function(){
console.log(name, 1);
callback();
},1000)
});
queue.tapAsync('2',function(name,callback){
setTimeout(function(){
console.log(name, 2);
callback();
},2000)
});
queue.tapAsync('3',function(name,callback){
setTimeout(function(){
console.log(name, 3);
callback();
},3000)
});
queue.callAsync('bearbao',err=>{
console.log(err);
console.timeEnd('cost');
});
// 輸出結果
/*
bearbao 1
bearbao 2
bearbao 3
cost: 3000.448974609375ms
*/
複製程式碼
實現
class AsyncParallelHook {
constructor(){
this.listeners = [];
}
tapAsync(name,listener){
this.listeners.push(listener);
}
callAsync(...arg){
let callback = arg.pop();
let i = 0;
let done = ()=>{
if(++i==this.listeners.length){
callback()
}
}
this.listeners.forEach(listener=>listener(...arg,done));
}
}
複製程式碼
- 使用tapPromise
使用tapPromise註冊監聽時,每個回撥函式的返回值必須是一個Promise的例項
let queue = new AsyncParallelHook(['name']);
console.time('cost');
queue.tapPromise('1',function(name){
return new Promise(function(resolve,reject){
setTimeout(function(){
console.log(1);
resolve();
},1000)
});
});
queue.tapPromise('2',function(name){
return new Promise(function(resolve,reject){
setTimeout(function(){
console.log(2);
resolve();
},2000)
});
});
queue.tapPromise('3',function(name){
return new Promise(function(resolve,reject){
setTimeout(function(){
console.log(3);
resolve();
},3000)
});
});
queue.promise('bearbao').then(()=>{
console.timeEnd('cost');
})
// 執行記過
/*
1
2
3
cost: 3000.448974609375ms
*/
複製程式碼
實現
class AsyncParallelHook {
constructor(){
this.listeners = [];
}
tapPromise(name,listener){
this.listeners.push(listener);
}
promise(...arg){
let i = 0;
return Promise.all(this.listeners.map(l=>l(arg)))
}
}
複製程式碼
5. 好睏好睏
一不小心又到1點了,為了能夠獲得長壽成就,今天就先寫到這裡吧,後續幾個方法,過兩天再更新上來
結語
如果覺得還可以,能在諸君的編碼之路上帶來一點幫助,請點贊鼓勵一下,謝謝!