webpack怎麼能只是會用呢,核心中的核心tapable瞭解下?

鮑康霖發表於2018-07-25

前言

為什麼我們要學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函式的解讀,裡面有詳細的介紹,這裡就不多加贅述了

Redux進階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點了,為了能夠獲得長壽成就,今天就先寫到這裡吧,後續幾個方法,過兩天再更新上來

結語

如果覺得還可以,能在諸君的編碼之路上帶來一點幫助,請點贊鼓勵一下,謝謝!

相關文章