學習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的調用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
}
}
}
}
複製代碼
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
oop
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
的執行流程,那咱們必需要知道它們執行完的時機:
使用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
的最後一個參數參數爲一個函數,而且在異步任務執行完畢後執行它,這樣咱們能獲取執行完畢的時機,以下例所示:
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
時,重點不在於這個庫的細節和使用,而在於多個任務有可能的執行流程以及流程的實現原理,它們是衆多實際問題的抽象模型,掌握了它們,你就能夠在實際開發中和麪試中觸類旁通,舉重若輕。