上一章咱們有提到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兩個類。markdown
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') })複製代碼