webpack事件流之Tapable

摘要

上一章咱們有提到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兩個類。併發

  • Hook基類主要收集並處理掛載在鉤子上的taps以及interceptors
  • HookCodeFactory基類根據前者返回的options生成執行鉤子的代碼

實現
 1. SyncHook
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')複製代碼

2. SyncBailHook
// 只須要在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')複製代碼

3. AsyncSeriesHook
// 只需在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')
})複製代碼
相關文章
相關標籤/搜索