Webpack Tapable原理詳解
directory
- src - sim---- 簡單的模擬實現 - /.js$/ ---- 使用
Detailed
Webpack 就像一條生產線, 要經過一系列的處理流程才能將原始檔轉換成輸出結果。這條生產線上的每個流程都是單一的, 多個流程之間存在依賴關係。只能完成當前處理後才會轉交到下一個流程。
外掛就像一個插入到生產線中的一個功能, 它會在特定的時機對生產線上的資源進行處理。
這條生產線很複雜, Webpack則是通過 tapable
核心庫來組織這條生產線。
Webpack 在執行中會通過 tapable
提供的鉤子進行廣播事件, 外掛只需要監聽它關心的事件,就可以加入到這條生產線中,去改變生產線的運作。使得 Webpack整體擴充套件性很好。
Tapable Hook
Tapable 提供同步(Sync)和非同步(Async)鉤子類。而非同步又分為 非同步序列
、 非同步並行
鉤子類。
右鍵圖片,在新標籤中檢視完整圖片
逐個分析每個鉤子類的使用及其原理
同步鉤子類
- SyncHook
- SyncBailHook
- SyncWaterfallHook
- SyncLoopHook
同步鉤子類通過例項的 tap
方法監聽函式, 通過 call
釋出事件
SyncHook
同步序列不關心訂閱函式執行後的返回值是什麼。其原理是將監聽(訂閱)的函式存放到一個數組中, 釋出時遍歷陣列中的監聽函式並且將釋出時的 arguments
傳遞給監聽函式
class SyncHook { constructor(options) { this.options = options this.hooks = []//存放監聽函式的陣列 } tap(name, callback) { this.hooks.push(callback) } call(...args) { for (let i = 0; i < this.hooks.length; i++) { this.hooks[i](...args) } } } const synchook = new SyncHook('name') // 註冊監聽函式 synchook.tap('name', (data) => { console.log('name', data) }) synchook.tap('age', (data) => { console.log('age', data) }) // 釋出事件 synchook.call('qiqingfu')
列印結果:
name qiqingfu age qiqingfu
SyncBailHook
同步序列, 但是如果監聽函式的返回值不為 null
, 就終止後續的監聽函式執行
class SyncBailHook { constructor(options) { this.options = options this.hooks = [] } tap(name, callback) { this.hooks.push(callback) } call(...args) { let ret, i = 0 do { // 將第一個函式的返回結果賦值給ret, 在while中如果結果為 true就繼續執行do程式碼塊 ret = this.hooks[i++](...args) } while(!ret) } } const syncbailhook = new SyncBailHook('name') syncbailhook.tap('name', (data) => { console.log('name', data) return '我的返回值不為null' }) syncbailhook.tap('age', (data) => { console.log('age', data) }) syncbailhook.call('qiqingfu')
執行結果
name qiqingfu
SyncWaterfallHook
同步序列瀑布流, 瀑布流指的是第一個監聽函式的返回值,做為第二個監聽函式的引數。第二個函式的返回值作為第三個監聽函式的引數,依次類推...
class SyncWaterfallHook { constructor(options) { this.options = options this.hooks = [] } tap(name, callback) { this.hooks.push(callback) } call(...args) { let [firstHook, ...otherHooks] = this.hooks /** * 通過解構賦值先取出第一個監聽函式執行 * 並且將第一個函式的執行結果傳遞給第二個, 第二個傳遞給第三個,迭代的過程 */ let ret = firstHook(...args) otherHooks.reduce((f,n) => { return n(f) }, ret) } } const syncWaterfallHook = new SyncWaterfallHook('name') syncWaterfallHook.tap('name', data => { console.log('name', data) return 23 }) syncWaterfallHook.tap('age', data => { console.log('age', data) }) syncWaterfallHook.call('qiqingfu')
列印結果
name qiqingfu age 23
SyncLoopHook
同步序列, 如果監聽函式的返回值為 true
, 則反覆執行當前的監聽函式,直到返回指為 undefind
則繼續執行下面的監聽函式
class SyncLoopHook { constructor(options) { this.options = options this.hooks = [] } tap(name, callback) { this.hooks.push(callback) } call(...args) { for (let i = 0; i < this.hooks.length; i++) { let hook = this.hooks[i], ret do{ ret = hook(...args) }while(ret === true && ret !== undefined) } } } const syncLoopHook = new SyncLoopHook('name') let n1 = 0 syncLoopHook.tap('name', data => { console.log('name', data) return n1 < 2 ? true : undefined }) syncLoopHook.tap('end', data => { console.log('end', data) }) syncLoopHook.call('qiqingfu')
執行結果
name qiqingfu name qiqingfu name qiqingfu第三次列印的時候, n1的指為2, 返回值為 undefined則執行後面的監聽函式 end qiqingfu
非同步鉤子
- 非同步並行
(Parallel)
- AsyncParallelHook
- AsyncParalleBailHook
- 非同步序列
(Series)
- AsyncSeriesHook
- AsyncSeriesBailHook
- AsyncSeriesWaterfallHook
凡有非同步,必有回撥
同步鉤子是通過 tap
來監聽函式的, call
來發布的。
非同步鉤子是通過 tapAsync
或 tapPromise
來監聽函式,通過 callAsync
或 promise
來發布訂閱的。
AsyncParallelHook
非同步並行, 監聽的函式會一塊執行, 哪個函式先執行完就先觸發。不需要關心監聽函式的返回值。
class AsyncParallelHook { constructor(options) { this.options = options this.asyncHooks = [] } // 訂閱 tapAsync(name, callback) { this.asyncHooks.push(callback) } // 釋出 callAsync(...args) { /** * callAsync(arg1, arg2,..., cb) * 釋出的時候最後一個引數可以是回撥函式 * 訂閱的每一個函式的最後一個引數也是一個回撥函式,所有的訂閱函式執行完 * 且都呼叫了最後一個函式,才會執行cb */ const finalCallback = args.pop() let i = 0 // 將這個作為最後一個引數傳過去,使用的時候選擇性呼叫 const done = () => { ++i === this.asyncHooks.length && finalCallback() } this.asyncHooks.forEach(hook => { hook(...args, done) }) } } const asyncParallelHook = new AsyncParallelHook('name') asyncParallelHook.tapAsync('name', (data, done) => { setTimeout(() => { console.log('name', data) done() }, 2000) }) asyncParallelHook.tapAsync('age', (data, done) => { setTimeout(() => { console.log('age', data) done() }, 3000) }) console.time('time') asyncParallelHook.callAsync('qiqingfu', () => { console.log('監聽函式都呼叫了 done') console.timeEnd('time') })
列印結果
name qiqingfu age qiqingfu 監聽函式都呼叫了 done time: 3002.691ms
AsyncParalleBailHook
暫時不理解
AsyncSeriesHook
非同步序列鉤子類, 不關心 callback
的引數。非同步函式一個一個的執行,但是必須呼叫 done函式。
class AsyncSeriesHook { constructor(options) { this.options = options this.asyncHooks = [] } tapAsync(name, callback) { this.asyncHooks.push(callback) } callAsync(...args) { const finalCallback = args.pop() let i = 0 const done = () => { let task = this.asyncHooks[i++] task ? task(...args, done) : finalCallback() } done() } } const asyncSeriesHook = new AsyncSeriesHook('name') asyncSeriesHook.tapAsync('name', (data, done) => { setTimeout(() => { console.log('name', data) done() }, 1000) }) asyncSeriesHook.tapAsync('age', (data, done) => { setTimeout(() => { console.log('age', data) done() }, 2000) }) console.time('time') asyncSeriesHook.callAsync('qiqingfu', () => { console.log('end') console.timeEnd('time') })
執行結果
name qiqingfu age qiqingfu end time: 3010.915ms
AsyncSeriesBailHook
同步序列鉤子類, callback的引數如果不是 null
, 後面所有的非同步函式都不會執行,直接執行 callAsync
方法的回撥函式
class AsyncSeriesBailHook { constructor(options) { this.options = options this.asyncHooks = [] } tapAsync(name, callback) { this.asyncHooks.push(callback) } callAsync(...args) { const finalCallback = args.pop() let i = 0 const done = data => { if (data) return finalCallback() let task = this.asyncHooks[i++] task ? task(...args, done) : finalCallback() } done() } } const asyncSeriesBailHook = new AsyncSeriesBailHook('name') asyncSeriesBailHook.tapAsync('1', (data, done) => { setTimeout(() => { console.log('1', data) done(null) }, 1000) }) asyncSeriesBailHook.tapAsync('2', (data, done) => { setTimeout(() => { console.log('2', data) done(null) }, 2000) }) console.time('times') asyncSeriesBailHook.callAsync('qiqingfu', () => { console.log('end') console.timeEnd('times') })
列印結果
1 qiqingfu 2 qiqingfu end times: 3012.060ms
AsyncSeriesWaterfallHook
同步序列鉤子類, 上一個監聽函式 callback(err, data)的第二個引數, 可以作為下一個監聽函式的引數
class AsyncSeriesWaterfallHook { constructor(options) { this.options = options this.asyncHooks = [] } tapAsync(name, callback) { this.asyncHooks.push(callback) } callAsync(...args) { const finalCallback = args.pop() let i = 0, once const done = (err, data) => { let task = this.asyncHooks[i++] if (!task) return finalCallback() if (!once) { // 只執行一次 task(...args, done) once = true } else { task(data, done) } } done() } } const asyncSeriesWaterfallHook = new AsyncSeriesWaterfallHook('name') asyncSeriesWaterfallHook.tapAsync('1', (data, done) => { setTimeout(() => { console.log('1', data) done(null, '第一個callback傳遞的引數') }, 1000) }) asyncSeriesWaterfallHook.tapAsync('2', (data, done) => { setTimeout(() => { console.log('2', data) done(null) }, 1000) }) console.time('timer') asyncSeriesWaterfallHook.callAsync('qiqingfu', () => { console.log('end') console.timeEnd('timer') })
列印結果
1 qiqingfu 2 第一個callback傳遞的引數 end timer: 2015.445ms
END
如果理解有誤, 麻煩糾正!