學習webpack
源碼時,老是繞不開tapable
,越看越以爲它晦澀難懂,但只要理解了它的功能,學習就會容易不少。webpack
簡單來講,有一系列的同步、異步任務,我但願它們能夠以多種流程執行,好比:web
而tapable
庫,就幫咱們實現了多種任務的執行流程,它們能夠根據如下特色分類:面試
task
是否包含異步代碼task
是否有執行順序task
是否有數據依賴舉個例子,若是咱們想要多個同步的任務 串行執行,只須要三個步驟:初始化hook、添加任務、觸發任務執行:設計模式
// 引入 同步 的hook const { SyncBailHook } = require("tapable"); // 初始化 const tasks = new SyncBailHook(['tasks']) // 綁定一個任務 tasks.tap('task1', () => { console.log('task1', name); }) // 再綁定一個任務 tasks.tap('task2', () => { console.log('task2', name); }) // 調用call,咱們的兩個任務就會串行執行了, tasks.call('done')
是否是很簡單,下面咱們學習下tapable
實現了哪些任務執行流程,而且是如何實現的:promise
如上例子所示,每一種hook
都會有兩個方法,用於添加任務和觸發任務執行。在同步的hook
中,分別對應tap
和call
方法。併發
全部任務一塊兒執行
class SyncHook { constructor() { // 用於保存添加的任務 this.tasks = [] } tap(name, task) { // 註冊事件 this.tasks.push(task) } call(...args) { // 把註冊的事件依次調用,無特殊處理 this.tasks.forEach(task => task(...args)) } }
若是其中一個task
有返回值(不爲undefined
),就會中斷tasks的調用
class SyncBailHook { constructor() { // 用於保存添加的任務 this.tasks = [] } tap(name, task) { this.tasks.push(task) } call(...args) { for (let i = 0; i < this.tasks.length; i++) { const result = this.tasks[i](...args) // 有返回值的話,就會中斷調用 if (result !== undefined) { break } } } }
task
的計算結果會做爲下一個task
的參數,以此類推
class SyncWaterfallHook { constructor() { this.tasks = [] } tap(name, task) { this.tasks.push(task) } call(...args) { const [first, ...others] = this.tasks const result = first(...args) // 上一個task的返回值會做爲下一個task的函數參數 others.reduce((result, task) => { return task(result) }, result) } }
若是task
有返回值(返回值不爲undefined
),就會循環執行當前task
,直到返回值爲undefined
纔會執行下一個task
class SyncLoopHook { constructor() { this.tasks = [] } tap(name, task) { this.tasks.push(task) } call(...args) { // 當前執行task的index let currentTaskIdx = 0 while (currentTaskIdx < this.tasks.length) { let task = this.tasks[currentTaskIdx] const result = task(...args) // 只有返回爲undefined的時候纔會執行下一個task,不然一直執行當前task if (result === undefined) { currentTaskIdx++ } } } }
異步事件流中,綁定和觸發的方法都會有兩種實現:異步
promise
:tapPromise
綁定、promise
觸發promise
: tapAsync
綁定、callAsync
觸發既然咱們要控制異步tasks
的執行流程,那咱們必需要知道它們執行完的時機:async
使用promise
的hook
,任務中resolve
的調用就表明異步執行完畢了;函數
// 使用promise方法的例子 // 初始化異步並行的hook const asyncHook = new AsyncParallelHook('async') // 添加task // tapPromise須要返回一個promise asyncHook.tapPromise('render1', (name) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('render1', name); resolve() }, 1000); }) }) // 再添加一個task // tapPromise須要返回一個promise asyncHook.tapPromise('render2', (name) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('render2', name); resolve() }, 1000); }) }) // 傳入的兩個異步任務就能夠串行執行了,並在執行完畢後打印done asyncHook.promise().then( () => { console.log('done'); })
但在使用非promise
的hook
時,異步任務執行完畢的時機咱們就無從獲取了。因此咱們規定傳入的 task
的最後一個參數參數爲一個函數,而且在異步任務執行完畢後執行它,這樣咱們能獲取執行完畢的時機,以下例所示:oop
const asyncHook = new AsyncParallelHook('async') // 添加task asyncHook.tapAsync('example', (data, cb) => { setTimeout(() => { console.log('example', name); // 在異步操做完成時,調用回調函數,表示異步任務完成 cb() }, 1000); }) // 添加task asyncHook.tapAsync('example1', (data, cb) => { setTimeout(() => { console.log('example1', name); // 在異步操做完成時,調用回調函數,表示異步任務完成 cb() }, 1000); }) // 傳入的兩個異步任務就能夠串行執行了,並在執行完畢後打印done asyncHook.callAsync('done', () => { console.log('done') })
task
一塊兒執行,全部異步事件執行完成後,執行最後的回調。相似promise.all
NOTE: callAsync
中計數器的使用,相似於promise.all
的實現原理
class AsyncParallelHook { constructor() { this.tasks = [] } tapAsync(name, task) { this.tasks.push(task) } callAsync(...args) { // 最後一個參數爲,流程結束的回調 const finalCB = args.pop() let index = 0 // 這就是每一個task執行完成時調用的回調函數 const CB = () => { ++index // 當這個回調函數調用的次數等於tasks的個數時,說明任務都執行完了 if (index === this.tasks.length) { // 調用流程結束的回調函數 finalCB() } } this.tasks.forEach(task => task(...args, CB)) } // task是一個promise生成器 tapPromise(name, task) { this.tasks.push(task) } // 使用promise.all實現 promise(...args) { const tasks = this.tasks.map(task => task(...args)) return Promise.all(tasks) } }
全部tasks
串行執行,一個tasks
執行完了在執行下一個
NOTE:callAsync
的實現與使用,相似於generate
執行器co
和async await
的原理
NOTE:promise
的實現與使用,就是面試中常見的 異步任務調度題 的正解。好比,實現每隔一秒打印1次,打印5次。
class AsyncSeriesHook { constructor() { this.tasks = [] } tapAsync(name, task) { this.tasks.push(task) } callAsync(...args) { const finalCB = args.pop() let index = 0 // 這就是每一個task異步執行完畢以後調用的回調函數 const next = () => { let task = this.tasks[index++] if (task) { // task執行完畢以後,會調用next,繼續執行下一個task,造成遞歸,直到任務所有執行完 task(...args, next) } else { // 任務完畢以後,調用流程結束的回調函數 finalCB() } } next() } tapPromise(name, task) { this.tasks.push(task) } promise(...args) { let [first, ...others] = this.tasks return others.reduce((p, n) =>{ // then函數中返回另外一個promise,能夠實現promise的串行執行 return p.then(() => n(...args)) },first(...args)) } }
異步task
串行執行,task
的計算結果會做爲下一個task
的參數,以此類推。task
執行結果經過cb
回調函數向下傳遞。
class AsyncWaterfallHook { constructor() { this.tasks = [] } tapAsync(name, task) { this.tasks.push(task) } callAsync(...args) { const [first] = this.tasks const finalCB = args.pop() let index = 1 // 這就是每一個task異步執行完畢以後調用的回調函數,其中ret爲上一個task的執行結果 const next = (error, ret) => { if(error !== undefined) { return } let task = this.tasks[index++] if (task) { // task執行完畢以後,會調用next,繼續執行下一個task,造成遞歸,直到任務所有執行完 task(ret, next) } else { // 任務完畢以後,調用流程結束的回調函數 finalCB(ret) } } first(...args, next) } tapPromise(name, task) { this.tasks.push(task) } promise(...args) { let [first, ...others] = this.tasks return others.reduce((p, n) =>{ // then函數中返回另外一個promise,能夠實現promise的串行執行 return p.then(() => n(...args)) }, first(...args)) } }
學了tapable
的一些hook
,你能擴展到不少東西:
promise.all
co
模塊async await
你均可以去實現,用於鞏固和拓展相關知識。
咱們在學習tapable
時,重點不在於這個庫的細節和使用,而在於多個任務有可能的執行流程以及流程的實現原理,它們是衆多實際問題的抽象模型,掌握了它們,你就能夠在實際開發中和麪試中觸類旁通,舉重若輕。