上一章咱們有提到webpack支持特定的配置,來監控其編譯進度,那麼這個機制是怎麼實現的呢?webpack
webpack整個構建週期,會涉及不少個階段,每一個階段都對應着一些節點,這些節點就是咱們常說的鉤子,每個鉤子上掛載着一些插件,能夠說整個webpack生態系統是由一系列的插件組成的。當主構建流程進行編譯打包的時候,會陸續觸發一些鉤子的call方法(至關於emitter),相應的插件(至關於listener)就會獲得執行,webpack將這個機制封裝爲一個庫,就是Tapable,webpack的核心對象Compiler和Complation均是Tapable的實例。web
Tapable提供了不少類型的hook,主要分爲兩大類:同步和異步,異步又分爲串行(前一個異步執行完纔會執行下一個)和並行(等待全部併發的異步事件執行完以後纔會執行最後的回調)。在webpack裏邊(Compiler和Compilation)主要使用了SyncHook、SyncBailHook、AsyncSeriesHook三種鉤子,所以這裏咱們只着重介紹這三種。api
咱們參考的版本是webpack中使用的版本v1.1.3,這裏主要是Hook.js、SyncHook.js、HookCodeFactory.js三個文件,能夠供你們參考。bash
源碼裏邊,每個文件對應一個類型的鉤子。每一種鉤子都是基於Hook和HookCodeFactory兩個類。併發
class SyncHook{
constructor(arg) {
if (Array.isArray(arg)) {
this.args = arg
} else {
this.args = [arg]
}
this.funs = []
this.taps = []
this.interceptors = []
}
intercept(interceptor) {
this.interceptors.push(Object.assign({}, interceptor))
// 若是有多個register攔截器 老是以最後一個攔截器爲準
if (interceptor.register) {
// 全部Tap對象依賴於register函數的返回值
for(let i in this.taps) {
this.taps[i] = interceptor.register(this.taps[i])
}
}
}
tap(option, fn) {
if (typeof option === 'string') {
option = { name: option }
}
if (typeof option !== 'object' || option === null) {
throw new Error("Invalid arguments to tap(options: Object, fn: function)");
}
option = Object.assign({ type: "sync", fn: fn }, option);
if (typeof option.name !== 'string' || option.name === '') {
throw new Error("Missing name for tap");
}
option = this._putInterceptor(option)
this.taps.push(option)
// 根據stage屬性進行排序 決定taps的觸發順序
if (this.taps.length > 1) {
this.taps = this._sort(this.taps)
}
this.funs = this.taps.map(item => item.fn)
}
call(...args) {
// 以初始化鉤子時傳入的參數長度爲準,多餘的參數無效
const _args = args.slice(0, this.args.length)
for(let i in this.funs) {
const curTap = this.taps[i]
const curFun = this.funs[i]
// 根據Tap對象的屬性 決定是否要傳入上下文
if (curTap.context) {
curFun.call(this, curTap, ..._args)
} else {
curFun.call(this, ..._args)
}
}
}
_putInterceptor(option) {
// 若tap的時候已經存在攔截器 則替換Tap對象
for(let interceptor of this.interceptors) {
if (interceptor.register) {
const newOption = interceptor.register(option)
if (!newOption) {
option = newOption
}
}
}
return option
}
_sort(taps) {
return taps.sort((cur, next) => cur.stage - next.stage)
}
}
// 使用
const hook = new SyncHook(["arg1",'arg2']);
hook.intercept({
register: (tap) => {
tap.name = 'changed'
return tap
},
})
hook.tap({
name: 'A',
context: true,
stage: 22,
}, (context, arg1) => {
console.log('A:', arg1)
})
hook.call('arg1', 'arg2')複製代碼
// 只須要在SyncHook的基礎上改動一下call方法
class SyncBailHook() {
// some code...
call(...args) {
// 以初始化鉤子時傳入的參數長度爲準,多餘的參數無效
const _args = args.slice(0, this.args.length)
let ret
for(let i in this.funs) {
if (ret !== undefined) {
break
} else {
const curTap = this.taps[i]
const curFun = this.funs[i]
// 根據Tap對象的屬性 決定是否要傳入上下文
if (curTap.context) {
ret = curFun.call(this, curTap, ..._args)
} else {
ret = curFun.call(this, ..._args)
}
}
}
}
// some code...
}
const hook = new SyncBailHook(['arg'])
hook.tap('A', (param) => {
console.log('A:',param)
// 只有當返回值爲undefined時 纔會執行後邊的鉤子
return 1
})
hook.tap('B', (param) => console.log('B:', param))
hook.call('hi')複製代碼
// 只需在SyncHook的基礎上新增兩個方法
class AsyncSeriesHook{
// some code ...
// 同tap方法
tapAsync(option, fn) {
// some code ...
}
callAsync(...args) {
const finalCb = args.pop()
let index = 0
let next = () => {
if (this.funs.length == index) {
return finalCb()
}
let curFun = this.funs[index++]
curFun(...args, next)
}
next()
}
// some code ...
}
const hook = new AsyncSeriesHook(['arg'])
hook.tapAsync('A', (param, cb) => {
setTimeout(() => {
console.log('A', param)
cb() // 必須調用cb 纔會執行後續的鉤子
}, 1000)
})
hook.tapAsync('B', (param, cb) => {
setTimeout(() => {
console.log('B')
cb()
}, 0)
})
hook.callAsync('hi', () => {
console.log('end')
})複製代碼