學(xué)習(xí)webpack
源碼時(shí)晴音,總是繞不開(kāi)tapable
,越看越覺(jué)得它晦澀難懂缔杉,但只要理解了它的功能锤躁,學(xué)習(xí)就會(huì)容易很多。
簡(jiǎn)單來(lái)說(shuō)或详,有一系列的同步系羞、異步任務(wù),我希望它們可以以多種流程執(zhí)行霸琴,比如:
- 一個(gè)執(zhí)行完再執(zhí)行下一個(gè)椒振,即串行執(zhí)行;
- 一塊執(zhí)行梧乘,即并行執(zhí)行澎迎;
- 串行執(zhí)行過(guò)程中,可以中斷執(zhí)行选调,即有熔斷機(jī)制
- 等等
而tapable
庫(kù)夹供,就幫我們實(shí)現(xiàn)了多種任務(wù)的執(zhí)行流程,它們可以根據(jù)以下特點(diǎn)分類(lèi):
-
同步sync学歧、異步async**:
task
是否包含異步代碼 -
串行series罩引、并發(fā)parallel**:前后
task
是否有執(zhí)行順序 - 是否使用promise
- 熔斷bail**:是否有熔斷機(jī)制
-
waterfall:前后
task
是否有數(shù)據(jù)依賴(lài)
舉個(gè)例子,如果我們想要多個(gè)同步的任務(wù) 串行執(zhí)行枝笨,只需要三個(gè)步驟:初始化hook、添加任務(wù)揭蜒、觸發(fā)任務(wù)執(zhí)行:
// 引入 同步 的hook
const { SyncBailHook } = require("tapable");
// 初始化
const tasks = new SyncBailHook(['tasks'])
// 綁定一個(gè)任務(wù)
tasks.tap('task1', () => {
console.log('task1', name);
})
// 再綁定一個(gè)任務(wù)
tasks.tap('task2', () => {
console.log('task2', name);
})
// 調(diào)用call横浑,我們的兩個(gè)任務(wù)就會(huì)串行執(zhí)行了,
tasks.call('done')
是不是很簡(jiǎn)單屉更,下面我們學(xué)習(xí)下tapable
實(shí)現(xiàn)了哪些任務(wù)執(zhí)行流程徙融,并且是如何實(shí)現(xiàn)的:
一、同步事件流
如上例子所示瑰谜,每一種hook
都會(huì)有兩個(gè)方法欺冀,用于添加任務(wù)和觸發(fā)任務(wù)執(zhí)行。在同步的hook
中萨脑,分別對(duì)應(yīng)tap
和call
方法隐轩。
1. 并行
所有任務(wù)一起執(zhí)行
class SyncHook {
constructor() {
// 用于保存添加的任務(wù)
this.tasks = []
}
tap(name, task) {
// 注冊(cè)事件
this.tasks.push(task)
}
call(...args) {
// 把注冊(cè)的事件依次調(diào)用,無(wú)特殊處理
this.tasks.forEach(task => task(...args))
}
}
2. 串行可熔斷
如果其中一個(gè)
task
有返回值(不為undefined
)渤早,就會(huì)中斷tasks的調(diào)用
class SyncBailHook {
constructor() {
// 用于保存添加的任務(wù)
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)
// 有返回值的話(huà)职车,就會(huì)中斷調(diào)用
if (result !== undefined) {
break
}
}
}
}
3. 串行瀑布流
task
的計(jì)算結(jié)果會(huì)作為下一個(gè)task
的參數(shù),以此類(lèi)推
class SyncWaterfallHook {
constructor() {
this.tasks = []
}
tap(name, task) {
this.tasks.push(task)
}
call(...args) {
const [first, ...others] = this.tasks
const result = first(...args)
// 上一個(gè)task的返回值會(huì)作為下一個(gè)task的函數(shù)參數(shù)
others.reduce((result, task) => {
return task(result)
}, result)
}
}
4. 串行可循環(huán)
如果
task
有返回值(返回值不為undefined
),就會(huì)循環(huán)執(zhí)行當(dāng)前task
悴灵,直到返回值為undefined
才會(huì)執(zhí)行下一個(gè)task
class SyncLoopHook {
constructor() {
this.tasks = []
}
tap(name, task) {
this.tasks.push(task)
}
call(...args) {
// 當(dāng)前執(zhí)行task的index
let currentTaskIdx = 0
while (currentTaskIdx < this.tasks.length) {
let task = this.tasks[currentTaskIdx]
const result = task(...args)
// 只有返回為undefined的時(shí)候才會(huì)執(zhí)行下一個(gè)task扛芽,否則一直執(zhí)行當(dāng)前task
if (result === undefined) {
currentTaskIdx++
}
}
}
}
二、異步事件流
異步事件流中积瞒,綁定和觸發(fā)的方法都會(huì)有兩種實(shí)現(xiàn):
- 使用
promise
:tapPromise
綁定川尖、promise
觸發(fā) - 非
promise
:tapAsync
綁定、callAsync
觸發(fā)
注意事項(xiàng):
既然我們要控制異步tasks
的執(zhí)行流程茫孔,那我們必須要知道它們執(zhí)行完的時(shí)機(jī):
-
使用
promise
的hook
空厌,任務(wù)中resolve
的調(diào)用就代表異步執(zhí)行完畢了;// 使用promise方法的例子 // 初始化異步并行的hook const asyncHook = new AsyncParallelHook('async') // 添加task // tapPromise需要返回一個(gè)promise asyncHook.tapPromise('render1', (name) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('render1', name); resolve() }, 1000); }) }) // 再添加一個(gè)task // tapPromise需要返回一個(gè)promise asyncHook.tapPromise('render2', (name) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('render2', name); resolve() }, 1000); }) }) // 傳入的兩個(gè)異步任務(wù)就可以串行執(zhí)行了银酬,并在執(zhí)行完畢后打印done asyncHook.promise().then( () => { console.log('done'); })
-
但在使用非
promise
的hook
時(shí)嘲更,異步任務(wù)執(zhí)行完畢的時(shí)機(jī)我們就無(wú)從獲取了。所以我們規(guī)定傳入的task
的最后一個(gè)參數(shù)參數(shù)為一個(gè)函數(shù)揩瞪,并且在異步任務(wù)執(zhí)行完畢后執(zhí)行它赋朦,這樣我們能獲取執(zhí)行完畢的時(shí)機(jī),如下例所示:const asyncHook = new AsyncParallelHook('async') // 添加task asyncHook.tapAsync('example', (data, cb) => { setTimeout(() => { console.log('example', name); // 在異步操作完成時(shí)李破,調(diào)用回調(diào)函數(shù)宠哄,表示異步任務(wù)完成 cb() }, 1000); }) // 添加task asyncHook.tapAsync('example1', (data, cb) => { setTimeout(() => { console.log('example1', name); // 在異步操作完成時(shí),調(diào)用回調(diào)函數(shù)嗤攻,表示異步任務(wù)完成 cb() }, 1000); }) // 傳入的兩個(gè)異步任務(wù)就可以串行執(zhí)行了毛嫉,并在執(zhí)行完畢后打印done asyncHook.callAsync('done', () => { console.log('done') })
1. 并行執(zhí)行
task
一起執(zhí)行,所有異步事件執(zhí)行完成后妇菱,執(zhí)行最后的回調(diào)承粤。類(lèi)似promise.all
NOTE: callAsync
中計(jì)數(shù)器的使用,類(lèi)似于promise.all
的實(shí)現(xiàn)原理
class AsyncParallelHook {
constructor() {
this.tasks = []
}
tapAsync(name, task) {
this.tasks.push(task)
}
callAsync(...args) {
// 最后一個(gè)參數(shù)為闯团,流程結(jié)束的回調(diào)
const finalCB = args.pop()
let index = 0
// 這就是每個(gè)task執(zhí)行完成時(shí)調(diào)用的回調(diào)函數(shù)
const CB = () => {
++index
// 當(dāng)這個(gè)回調(diào)函數(shù)調(diào)用的次數(shù)等于tasks的個(gè)數(shù)時(shí)辛臊,說(shuō)明任務(wù)都執(zhí)行完了
if (index === this.tasks.length) {
// 調(diào)用流程結(jié)束的回調(diào)函數(shù)
finalCB()
}
}
this.tasks.forEach(task => task(...args, CB))
}
// task是一個(gè)promise生成器
tapPromise(name, task) {
this.tasks.push(task)
}
// 使用promise.all實(shí)現(xiàn)
promise(...args) {
const tasks = this.tasks.map(task => task(...args))
return Promise.all(tasks)
}
}
2. 異步串行執(zhí)行
所有
tasks
串行執(zhí)行,一個(gè)tasks
執(zhí)行完了在執(zhí)行下一個(gè)
NOTE:callAsync
的實(shí)現(xiàn)與使用房交,類(lèi)似于generate
執(zhí)行器co
和async await
的原理
NOTE:promise
的實(shí)現(xiàn)與使用彻舰,就是面試中常見(jiàn)的 異步任務(wù)調(diào)度題 的正解。比如候味,實(shí)現(xiàn)每隔一秒打印1次刃唤,打印5次。
class AsyncSeriesHook {
constructor() {
this.tasks = []
}
tapAsync(name, task) {
this.tasks.push(task)
}
callAsync(...args) {
const finalCB = args.pop()
let index = 0
// 這就是每個(gè)task異步執(zhí)行完畢之后調(diào)用的回調(diào)函數(shù)
const next = () => {
let task = this.tasks[index++]
if (task) {
// task執(zhí)行完畢之后白群,會(huì)調(diào)用next尚胞,繼續(xù)執(zhí)行下一個(gè)task,形成遞歸川抡,直到任務(wù)全部執(zhí)行完
task(...args, next)
} else {
// 任務(wù)完畢之后辐真,調(diào)用流程結(jié)束的回調(diào)函數(shù)
finalCB()
}
}
next()
}
tapPromise(name, task) {
this.tasks.push(task)
}
promise(...args) {
let [first, ...others] = this.tasks
return others.reduce((p, n) =>{
// then函數(shù)中返回另一個(gè)promise须尚,可以實(shí)現(xiàn)promise的串行執(zhí)行
return p.then(() => n(...args))
},first(...args))
}
}
3. 串行瀑布流
異步
task
串行執(zhí)行,task
的計(jì)算結(jié)果會(huì)作為下一個(gè)task
的參數(shù)侍咱,以此類(lèi)推耐床。task
執(zhí)行結(jié)果通過(guò)cb
回調(diào)函數(shù)向下傳遞。
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
// 這就是每個(gè)task異步執(zhí)行完畢之后調(diào)用的回調(diào)函數(shù)楔脯,其中ret為上一個(gè)task的執(zhí)行結(jié)果
const next = (error, ret) => {
if(error !== undefined) {
return
}
let task = this.tasks[index++]
if (task) {
// task執(zhí)行完畢之后撩轰,會(huì)調(diào)用next,繼續(xù)執(zhí)行下一個(gè)task昧廷,形成遞歸堪嫂,直到任務(wù)全部執(zhí)行完
task(ret, next)
} else {
// 任務(wù)完畢之后,調(diào)用流程結(jié)束的回調(diào)函數(shù)
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函數(shù)中返回另一個(gè)promise木柬,可以實(shí)現(xiàn)promise的串行執(zhí)行
return p.then(() => n(...args))
}, first(...args))
}
}
總結(jié)
學(xué)了tapable
的一些hook
皆串,你能擴(kuò)展到很多東西:
promise.all
-
co
模塊 async await
- 面試中的經(jīng)典手寫(xiě)代碼題:任務(wù)調(diào)度系列
- 設(shè)計(jì)模式之監(jiān)聽(tīng)者模式
- 設(shè)計(jì)模式之發(fā)布訂閱者模式
你都可以去實(shí)現(xiàn),用于鞏固和拓展相關(guān)知識(shí)眉枕。
我們?cè)趯W(xué)習(xí)tapable
時(shí)恶复,重點(diǎn)不在于這個(gè)庫(kù)的細(xì)節(jié)和使用,而在于多個(gè)任務(wù)有可能的執(zhí)行流程以及流程的實(shí)現(xiàn)原理速挑,它們是眾多實(shí)際問(wèn)題的抽象模型谤牡,掌握了它們,你就可以在實(shí)際開(kāi)發(fā)中和面試中舉一反三姥宝,舉重若輕翅萤。