webpack中tapable原理詳解,一塊兒學習任務流程管理

學習webpack源碼時,老是繞不開tapable,越看越以爲它晦澀難懂,但只要理解了它的功能,學習就會容易不少。webpack

簡單來講,有一系列的同步、異步任務,我但願它們能夠以多種流程執行,好比:web

  • 一個執行完再執行下一個,即串行執行
  • 一塊執行,即並行執行
  • 串行執行過程當中,能夠中斷執行,即有熔斷機制
  • 等等

tapable庫,就幫咱們實現了多種任務的執行流程,它們能夠根據如下特色分類:面試

  • 同步sync、異步asynctask是否包含異步代碼
  • 串行series、併發parallel:先後task是否有執行順序
  • 是否使用promise
  • 熔斷bail:是否有熔斷機制
  • waterfall:先後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

1、同步事件流

如上例子所示,每一種hook都會有兩個方法,用於添加任務觸發任務執行。在同步的hook中,分別對應tapcall方法。併發

1. 並行

全部任務一塊兒執行異步

class SyncHook {
    constructor() {
        // 用於保存添加的任務
        this.tasks = []
    }

    tap(name, task) {
        // 註冊事件
        this.tasks.push(task)
    }

    call(...args) {
        // 把註冊的事件依次調用,無特殊處理
        this.tasks.forEach(task => task(...args))
    }
}
複製代碼

2. 串行可熔斷

若是其中一個task有返回值(不爲undefined),就會中斷tasks的調用async

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
            }
        }
    }
}

複製代碼

3. 串行瀑布流

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)
    }
}
複製代碼

4. 串行可循環

若是task有返回值(返回值不爲undefined),就會循環執行當前task,直到返回值爲undefined纔會執行下一個taskoop

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++
            }
        }
    }
}
複製代碼

2、異步事件流

異步事件流中,綁定和觸發的方法都會有兩種實現:

  • 使用promisetapPromise綁定、promise觸發
  • promisetapAsync綁定、callAsync觸發

注意事項:

既然咱們要控制異步tasks的執行流程,那咱們必需要知道它們執行完的時機:

  • 使用promisehook,任務中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');
    })
    複製代碼
  • 但在使用非promisehook時,異步任務執行完畢的時機咱們就無從獲取了。因此咱們規定傳入的 task的最後一個參數參數爲一個函數,而且在異步任務執行完畢後執行它,這樣咱們能獲取執行完畢的時機,以下例所示:

    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')
    })
    複製代碼

1. 並行執行

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)
    }
}

複製代碼

2. 異步串行執行

全部tasks串行執行,一個tasks執行完了在執行下一個

NOTE:callAsync的實現與使用,相似於generate執行器coasync 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))
    }
}
複製代碼

3. 串行瀑布流

異步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時,重點不在於這個庫的細節和使用,而在於多個任務有可能的執行流程以及流程的實現原理,它們是衆多實際問題的抽象模型,掌握了它們,你就能夠在實際開發中和麪試中觸類旁通,舉重若輕。

碰到過哪些流程管理方面的面試題呢?寫到評論區你們一塊兒討論下!!!

相關文章
相關標籤/搜索