Webpack Tapable原理詳解

apy發表於2019-02-16

directory

    - src
        - sim    ---- 簡單的模擬實現
        - /.js$/ ---- 使用

程式碼已上傳github, 地址

Detailed

Webpack 就像一條生產線, 要經過一系列的處理流程才能將原始檔轉換成輸出結果。這條生產線上的每個流程都是單一的, 多個流程之間存在依賴關係。只能完成當前處理後才會轉交到下一個流程。

外掛就像一個插入到生產線中的一個功能, 它會在特定的時機對生產線上的資源進行處理。

這條生產線很複雜, Webpack則是通過 tapable 核心庫來組織這條生產線。

Webpack 在執行中會通過 tapable 提供的鉤子進行廣播事件, 外掛只需要監聽它關心的事件,就可以加入到這條生產線中,去改變生產線的運作。使得 Webpack整體擴充套件性很好。

Tapable Hook

Tapable 提供同步(Sync)和非同步(Async)鉤子類。而非同步又分為 非同步序列非同步並行鉤子類。

右鍵圖片,在新標籤中檢視完整圖片

Tapable Hook Class

逐個分析每個鉤子類的使用及其原理

同步鉤子類

  • 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來發布的。

非同步鉤子是通過 tapAsynctapPromise 來監聽函式,通過 callAsyncpromise來發布訂閱的。

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

如果理解有誤, 麻煩糾正!

參考文章

webpack4.0原始碼分析之Tapable

webpack 4.0 Tapable 類中的常用鉤子函式原始碼分析

相關文章