webpack 4.0 Tapable 類中的經常使用鉤子函數源碼分析

引言

Tapable 是webpack中的基礎類,相似於node中的EventEmitter,都是註冊監聽,而後收發事件,監聽函數執行的過程,自身能夠被繼承或混入到其它模塊中。node

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

因此若是你想了解webpack的源碼,那麼先來了解一下Tapable這個基礎類顯得尤其必要,那接下來讓我帶你先來了解一下這個類上的經常使用的 9個鉤子函數吧!web

安裝

npm i Tapable -d
複製代碼

hooks分類

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

hooks詳解

每個鉤子都是一個構造函數,全部的構造函數都接收一個可選的參數 這個參數是一個數組,數組裏面能夠放一些參數 例如:‘name’,當觸發hook事件時 須要傳入name 參數,而後監聽函數中能夠獲取name參數。數組

const hook = new Hook([‘name’])
複製代碼
那怎麼註冊事件呢?

根據不一樣的鉤子函數使用不一樣的方法註冊事件,經常使用的註冊事件的方法有:tap, tapPromise, tapAsync。promise

那怎麼觸發事件呢?

一樣的,也是根據不一樣的鉤子函數 使用的觸發事件的方法也不相同,經常使用的觸發事件的方法有:call,promise, callAsync。bash

Sync*類型的hooks

註冊在該鉤子下面的插件的執行順序都是順序執行。 只能使用tap註冊,不能使用tapPromise和tapAsync註冊併發

一、SyncHook
串行同步執行 不關心返回值
複製代碼
用法
const { SyncHook } = require('tapable');
const mySyncHook = new SyncHook(['name', 'age']);
// 爲何叫tap水龍頭 接收兩個參數,第一個參數是名稱(備註:沒有任何意義)  第二個參數是一個函數 接收一個參數  name這個name和上面的name對應 age和上面的age對應
mySyncHook.tap('1', function (name, age) {
    console.log(name, age, 1)
    return 'wrong' // 不關心返回值 這裏寫返回值對結果沒有任何影響
});

mySyncHook.tap('2', function (name, age) {
    console.log(name, age, 2)
});

mySyncHook.tap('3', function (name, age) {
    console.log(name, age, 3)
});

mySyncHook.call('liushiyu', '18');
// 執行的結果
// liushiyu 18 1
// liushiyu 18 2
// liushiyu 18 3
複製代碼
SyncHook源碼大體實現
class SyncHook {
    constructor () {
        this.hooks = [];
    }
    tap (name, fn) {
        this.hooks.push(fn)
    }
    call () {
        this.hooks.forEach(hook => hook(...arguments));
    }
}
複製代碼
二、SyncBailHook
串行同步執行 有一個返回值不爲null就跳過剩下的邏輯
Bail 是保險的意思 有一個出錯就不往下執行了
複製代碼
用法 同SyncHook 的用法
const { SyncBailHook } = require('tapable');
const mySyncBailHook = new SyncBailHook(['name', 'age']);
// 輸出結果
// liushiyu 18 1
// return 的值不是null 因此剩下的邏輯就不執行了
複製代碼
SyncBailHook源碼大體實現
class SyncBailHook {
    constructor () {
        this.hooks = [];
    }
    tap (name, fn) {
        this.hooks.push(fn)
    }
    call () {
        for(let i=0; i<this.hooks.length; i++) {
            let hook = this.hooks[i];
            let result = hook(...arguments);
            if (result) {
                break;
            }
        }
    }
}
複製代碼
三、SyncWaterfallHook
下一個任務要拿到上一個任務的返回值
複製代碼
用法
const { SyncWaterfallHook } = require('tapable');
const mySyncWaterfallHook = new SyncWaterfallHook(['name']);

mySyncWaterfallHook.tap('1', function (name) {
    console.log(name, '1')
    return '1'
})
mySyncWaterfallHook.tap('2', function (name) {
    console.log(name, '2')
    return '2'
})
mySyncWaterfallHook.tap('3', function (name) {
    console.log(name, '3')
})

mySyncWaterfallHook.call('liu')
// 輸出結果
// liu 1
// 1 2
// 2 3
複製代碼
SyncWaterfallHook源碼大體實現
class SyncWaterfallHook {
    constructor () {
        this.hooks = [];
    }
    tap (name, fn) {
        this.hooks.push(fn)
    }
    call () {
        let result = null
        for(let i=0; i< this.hooks.length; i++) {
            let hook = this.hooks[i];
            if (!i) {
                result = hook(...arguments)
            } else {
                result = hook(result)
            }
        }
    }
}
複製代碼
四、SyncLoopHook
監聽函數返回true表示繼續循環,返回undefine表示結束循環
複製代碼
用法
const { SyncLoopHook } = require('tapable');
const mySyncLoopHook = new SyncLoopHook(['name']);

let count = 0;
mySyncLoopHook.tap('1', function (name) {
    console.log(count++);
    if (count < name) {
        return true
    } else {
        return
    }
});

mySyncLoopHook.call('4');
// 輸出結果
//0
//1
//2
//3
複製代碼
SyncLoopHook源碼大體實現
class SyncLoopHook {
    constructor () {
        this.hook;
    }
    tap (name, fn) {
        this.hook = fn
    }
    call () {
        let result = this.hook(...arguments);
        // do{
        //     result = this.hook(...arguments)
        // } while(result)
        while(result) {
            result = this.hook(...arguments)
        }
    }
}
複製代碼

Async*類型的hooks

支持tap、tapPromise、tapAsync註冊 每次都是調用tap、tapSync、tapPromise註冊不一樣類型的插件鉤子,經過調用call、callAsync 、promise方式調用。其實調用的時候爲了按照必定的執行策略執行,調用compile方法快速編譯出一個方法來執行這些插件。異步

異步併發執行

一、AsyncParallelHook
異步併發執行  
複製代碼
用法

有三種註冊方式函數

// 第一種註冊方式 tap
myAsyncParallelHook.tap('1', function (name) {
    console.log(1, name)
})

myAsyncParallelHook.tap('2', function (name) {
    console.log(2, name)
})

myAsyncParallelHook.tap('3', function (name) {
    console.log(3, name)
})

myAsyncParallelHook.callAsync('liu', function () {
    console.log('over')
});
// 1 'liu'
// 2 'liu'
// 3 'liu'
// over

// 第二種註冊方式 tapAsync  凡事有異步 必有回調
console.time('cost')
myAsyncParallelHook.tapAsync('1', function (name, callback) {
    setTimeout(function () {
        console.log(1, name)
        callback()
    }, 1000)
    // callback()
})

myAsyncParallelHook.tapAsync('2', function (name, callback) {
    setTimeout(function () {
        console.log(2, name)
        callback()
    }, 2000)
    // callback()
})

myAsyncParallelHook.tapAsync('3', function (name, callback) {
    setTimeout(function () {
        console.log(3, name)
        callback()
    }, 3000)
})

myAsyncParallelHook.callAsync('liu', () => {
    console.log('over')
    console.timeEnd('cost')
});
並行執行 花費的總時間是時間最長的那個
//1 'liu'
//2 'liu'
//3 'liu'
//over
//cost: 3005.083ms

// 第三種註冊方式 tapPromise
console.time('cost')
myAsyncParallelHook.tapPromise('1', function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(1, name)
            resolve()
        }, 1000)
    })
})

myAsyncParallelHook.tapPromise('2', function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(2, name)
            resolve()
        }, 2000)
    })
})

myAsyncParallelHook.tapPromise('3', function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(3, name)
            resolve()
        }, 3000)
    })
})

myAsyncParallelHook.promise('liu').then(function () {
    console.log('ok')
    console.timeEnd('cost')
}, function () {
    console.log('error')
    console.timeEnd('cost')
});

// 1 'liu'
// 2 'liu'
// 3 'liu'
// ok
// cost: 3001.903ms
複製代碼

...

二、AsyncParallelBailHook

有一個失敗了 其餘的都不用走了
複製代碼
用法
const { AsyncParallelBailHook } = require('tapable');
const myAsyncParallelBailHook = new AsyncParallelBailHook(['name']);

// 第一種註冊方式 tap
myAsyncParallelBailHook.tap('1', function (name) {
    console.log(1, name)
    return 'wrong'
})

myAsyncParallelBailHook.tap('2', function (name) {
    console.log(2, name)
})

myAsyncParallelBailHook.tap('3', function (name) {
    console.log(3, name)
})

myAsyncParallelBailHook.callAsync('liu', function () {
    console.log('over')
});
// 1 'liu'
// over

// 第二種註冊方式 tapAsync  凡事有異步 必有回調
console.time('cost')
myAsyncParallelBailHook.tapAsync('1', function (name, callback) {
    setTimeout(function () {
        console.log(1, name)
        return 'wrong';// 最後的回調就不會調用了
        callback()
    }, 1000)
    // callback()
})

myAsyncParallelBailHook.tapAsync('2', function (name, callback) {
    setTimeout(function () {
        console.log(2, name)
        callback()
    }, 2000)
    // callback()
})

myAsyncParallelBailHook.tapAsync('3', function (name, callback) {
    setTimeout(function () {
        console.log(3, name)
        callback()
    }, 3000)
})

myAsyncParallelBailHook.callAsync('liu', () => {
    console.log('over')
console.timeEnd('cost')
});

// 1 'liu'
// 2 'liu'
// 3 'liu'

// 第三種註冊方式 tapPromise
console.time('cost')
myAsyncParallelBailHook.tapPromise('1', function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(1, name)
            reject('wrong');// reject()的參數是一個不爲null的參數時,最後的回調就不會再調用了
        }, 1000)
    })
})

myAsyncParallelBailHook.tapPromise('2', function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(2, name)
            resolve()
        }, 2000)
    })
})

myAsyncParallelBailHook.tapPromise('3', function (name) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(3, name)
            resolve()
        }, 3000)
    })
})

myAsyncParallelBailHook.promise('liu').then(function () {
    console.log('ok')
    console.timeEnd('cost')
}, function () {
    console.log('error')
    console.timeEnd('cost')
});


// 1 'liu'
// error
// cost: 1006.030ms
// 2 'liu'
// 3 'liu'
複製代碼

異步串行執行

一、AsyncSeriesHook
用法
let { AsyncSeriesHook } = require('tapable');
let myAsyncSeriesHook = new AsyncSeriesHook(['name']);
console.time('coast')
myAsyncSeriesHook.tapAsync('1', function (name, cb) {
    setTimeout(function () {
        console.log('1', name)
        cb()
    }, 1000)
});

myAsyncSeriesHook.tapAsync('2', function (name, cb) {
    setTimeout(function () {
        console.log('2', name)
        cb()
    }, 2000)
});

myAsyncSeriesHook.tapAsync('3', function (name, cb) {
    setTimeout(function () {
        console.log('3', name)
        cb()
    }, 3000)
});

myAsyncSeriesHook.callAsync('liu', function () {
    console.log('over')
    console.timeEnd('coast')
})

// 1 liu
// 2 liu
// 3 liu
// over
// coast: 6010.515ms 異步串行執行消耗的時間是全部的總和
複製代碼
AsyncSeriesHook源碼大體實現
class AsyncSeriesHook{
    constructor() {
        this.hooks = [];
    }
    tapAsync () {
        this.hooks.push(arguments[arguments.length-1]);
    }
    callAsync () {
        let args = Array.from(arguments); //將傳進來的參數轉化爲數組
        let done = args.pop(); // 取出數組的最後一項 即成功後的回調函數
        let index = 0;
        let that = this;
        function next(err) {
            if (err) return done();
            let fn = that.hooks[index++];
            fn ? fn(...args, next) : done();
        }
        next()
    }
}
複製代碼
二、AsyncSeriesBailHook
用法
// 異步串行執行
let { AsyncSeriesBailHook } = require('tapable');
let myAsyncSeriesBailHook = new AsyncSeriesBailHook(['name']);
console.time('coast')
myAsyncSeriesBailHook.tapAsync('1', function (name, cb) {
    setTimeout(function () {
        console.log('1', name)
        cb('wrong')
    }, 1000)
});

myAsyncSeriesBailHook.tapAsync('2', function (name, cb) {
    setTimeout(function () {
        console.log('2', name)
        cb()
    }, 2000)
});

myAsyncSeriesBailHook.tapAsync('3', function (name, cb) {
    setTimeout(function () {
        console.log('3', name)
        cb()
    }, 3000)
});

myAsyncSeriesBailHook.callAsync('liu', function () {
    console.log('over')
    console.timeEnd('coast')
})
// 1 liu
// over
// coast: 1004.175ms
複製代碼
AsyncSeriesBailHook 源碼的大體實現
class AsyncSeriesBailHook{
    constructor() {
        this.hooks = [];
    }
    tapAsync () {
        this.hooks.push(arguments[arguments.length-1]);
    }
    callAsync () {
        let args = Array.from(arguments); //將傳進來的參數轉化爲數組
        let done = args.pop(); // 取出數組的最後一項 即成功後的回調函數
        let index = 0;
        let that = this;
        function next(err) {
            if (err) return done();
            let fn = that.hooks[index++];
            fn ? fn(...args, next) : done();
        }
        next()
    }
}
複製代碼
三、AsyncSeriesWaterfallHook
下一個任務要拿到上一個任務的返回值
複製代碼
用法
// 異步串行執行
let { AsyncSeriesWaterfallHook } = require('tapable');
let myAsyncSeriesWaterfallHook = new AsyncSeriesWaterfallHook(['name']);
console.time('coast')
myAsyncSeriesWaterfallHook.tapAsync('1', function (name, cb) {
    setTimeout(function () {
        console.log('1', name)
        cb(null, 'aa')
    }, 1000)
});

myAsyncSeriesWaterfallHook.tapAsync('2', function (name, cb) {
    setTimeout(function () {
        console.log('2', name)
        cb(null, 'bb')
    }, 2000)
});

myAsyncSeriesWaterfallHook.tapAsync('3', function (name, cb) {
    setTimeout(function () {
        console.log('3', name)
        cb(null, 'cc')
    }, 3000)
});

myAsyncSeriesWaterfallHook.callAsync('liu', function () {
    console.log('over')
    console.timeEnd('coast')
})

// 1 liu
// 2 aa
// 3 bb
// over
// coast: 6011.774ms
複製代碼
AsyncSeriesWaterfallHook 源碼的大體實現
class AsyncSeriesWaterfallHook{
    constructor() {
        this.hooks = [];
    }
    tapAsync () {
        this.hooks.push(arguments[arguments.length-1]);
    }
    callAsync () {
        let args = Array.from(arguments); //將傳進來的參數轉化爲數組
        let done = args.pop(); // 取出數組的最後一項 即成功後的回調函數
        let index = 0;
        let that = this;
        function next(err, data) {
            if(index>=that.hooks.length) return done();
            if (err) return done(err);
            let fn = that.hooks[index++];
            if (index == 1) {
                fn(...args, next)
            } else {
                fn(data, next)
            }
        }
        next()
    }
}
複製代碼
相關文章
相關標籤/搜索