tapable聽說了很久,終於下定決心繫統學習一下
Q1:tapable解決的問題?
- tapable是個獨立的庫
- webpack中大量使用了這個庫
- tapable主要是用來處理事件,解決的問題有點類似EventEmitter,不過功能更加強大
Q2:tapable方法有哪些?
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
複製程式碼
好的,方法一共是上述這麼多,第一眼看過去,懵逼樹下你和我,所以我們還是一點點來,一個個的分析、學習和了解
Q3:啥是SyncHook?
先來個使用的例子,例如前端開發者需要掌握哪些技能?
step1:首先我們要明確群體是前端開發
const {SyncHook}= require('tapable');
const FontEnd = new SyncHook();
複製程式碼
ok,就是上面這兩句,我們建立了個FontEnd前端開發
step2:前端開發需要掌握哪些技能,例如webpack、react對吧
FontEnd.tap('webpack',()=>{
console.log("get webpack")
});
FontEnd.tap('react',()=>{
console.log("get react")
});
複製程式碼
ok,上面的tap就是用來繫結事件的,為前端開發新增了兩個技能
step3:技能需要學習才能掌握,所以我們要有學習的動作
FontEnd.learn=()=>{
FontEnd.call()
};
FontEnd.learn();
複製程式碼
step4:檢視執行結果
get webpack
get react
複製程式碼
可以看到,通過上面的呼叫,我們的前端開發已經學會了react、webpack
step5:傳參
前面知道FontEnd這個群體,需要學react、webpack,但落到個人角度,究竟哪一個開發者掌握這些技能了呢?
const {SyncHook}= require('tapable');
const FontEnd = new SyncHook();
FontEnd.tap('webpack',(name)=>{
console.log(name+" get webpack")
});
FontEnd.tap('react',(name)=>{
console.log(name+" get react")
});
FontEnd.start=(name)=>{
FontEnd.call(name)
};
FontEnd.start('xiaoming');
複製程式碼
修改前面的程式碼,新增引數,預期是輸出xxx get react
step6: 檢視輸出結果
undefined get webpack
undefined get react
複製程式碼
最終結果是undefined,也就是引數沒傳進去
step7:為SyncHook新增約定引數
這是因為const FontEnd = new SyncHook();
建立SyncHook的時候沒有約定引數,只要為其新增引數即可,如下:
const {SyncHook}= require('tapable');
const FontEnd = new SyncHook(['name']);// 新增引數約定
FontEnd.tap('webpack',(name)=>{
console.log(name+" get webpack")
});
FontEnd.tap('react',(name)=>{
console.log(name+" get react")
});
FontEnd.start=(name)=>{
FontEnd.call(name)
};
FontEnd.start('xiaoming');
複製程式碼
最終輸出:
xiaoming get webpack
xiaoming get react
複製程式碼
SyncHook總結
- SyncHook目前來看比較像訂閱釋出
- 就像jquery中的add、fire方法,只不過這裡是tap、call
Q4:SyncHook如何實現?
SyncHook實現比較簡單,就是最簡單的訂閱釋出
class SyncHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tap(name,task){
this.tasks.push(task);
}
call(...args){
const param = args.slice(0,this.limit.length);
this.tasks.forEach(item=>item(...param));
}
}
複製程式碼
- limit是用來做引數校驗的
- tasks用來收集訂閱
- tap方法用來想tasks中新增方法
- call方法,先檢驗引數,然後再執行所有的已訂閱方法
總結:原理比較簡單,沒有太多技術含量,主要就是一個同步的鉤子函式
Q5:啥是SyncBailHook?
熔斷機制,如果前一個事件return true
,則不再執行下一個,還是前面的例子:
const {SyncBailHook} =require('tapable');
const FontEnd = new SyncBailHook(['name']);
FontEnd.tap('webpack',(name)=>{
console.log(name+" get webpack ")
});
FontEnd.tap('react',(name)=>{
console.log(name+" get react")
});
FontEnd.start=(...args)=>{
FontEnd.call(...args)
};
FontEnd.start('xiaoming');
複製程式碼
此時,把函式從SyncHook換成SyncBailHook,執行的結果沒有任何區別
but,思考一下,學習很容易會學不下去,所以修改一下我們的例子:
const {SyncBailHook} =require('tapable');
const FontEnd = new SyncBailHook(['name']);
FontEnd.tap('webpack',(name)=>{
console.log(name+" get webpack ")
return '學不動了啊!';
});
FontEnd.tap('react',(name)=>{
console.log(name+" get react")
});
FontEnd.start=(...args)=>{
FontEnd.call(...args)
};
FontEnd.start('xiaoming');
複製程式碼
此時僅輸出:
xiaoming get webpack
複製程式碼
後面的react沒有執行
總結:
- SyncBailHook主要解決的問題是條件阻塞
- 當訂閱事件符合某一判斷時,不再執行下面的流程
- 應用場景,場景不斷深入的場景,a、a+b、a+b+c、a+b+c+d這種場景
Q6:SyncBailHook如何實現?
SyncBailHook也十分簡單,還是之前那個例子:
class SyncBailHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tap(name,task){
this.tasks.push(task);
}
call(...args){
const param = args.slice(0,this.limit.length);
this.tasks.some(item=>item(...param));// 只改了一行
}
}
複製程式碼
可以看到,和上面SyncHook十分相似,無非就是把執行函式forEach,換成some,因為some是阻塞式執行,當返回true,則不會執行後面的內容
Q7:啥是SyncWaterfullHook?
還是先來個使用的例子,例如前端,技能都是一個個學的,要學完webpack再學react,例如:
const {SyncWaterfallHook} = require('tapable');
const FontEnd = new SyncWaterfallHook(['name']);
FontEnd.tap('webpack',(name)=>{
console.log(name+" get webpack ")
return '學完webpack了,該學react了';
});
FontEnd.tap('react',(name)=>{
console.log(name+" get react")
});
FontEnd.start=(...args)=>{
FontEnd.call(...args)
};
FontEnd.start('xiaoming');
複製程式碼
此時輸出:
xiaoming get webpack
學完webpack了,該學react了 get react
複製程式碼
- SyncWaterfallHook會將前一個任務的執行結果,傳遞給後一個
- 主要使用場景是處理邏輯之間相互依賴
- 實際效果和redux中的compose方法一毛一樣
Q8:SyncWaterfullHook如何實現?
class SyncWaterfallHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tap(name,task){
this.tasks.push(task);
}
call(...args){
const param = args.slice(0,this.limit.length);
const [first,...others] = this.tasks;
const ret = first(...param);
others.reduce((pre,next)=>{
return next(pre);
},ret)
}
}
複製程式碼
SyncWaterfallHook實現也比較簡單
- 完全按照redux的compose來實現就行
- 第一步,取出第一個執行,並拿到結果ret
- 第二步,將結果ret,當作reduce的引數傳遞進去
- 第三步,遍歷,不斷把引數傳給下一個函式
總結:SyncWaterfallHook主要還是用於函式之間對結果存在依賴的場景
Q9:啥是SyncLoopHook?
還是前面的例子,如果一次學不懂一門技術,那就要多學幾遍,例如:
const FontEnd = new SyncLoopHook(['name']);
let num = 0;
FontEnd.tap('webpack',(name)=>{
console.log(name+" get webpack ")
return ++num === 3?undefined:'再學一次';
});
FontEnd.tap('react',(name)=>{
console.log(name+" get react")
});
FontEnd.start=(...args)=>{
FontEnd.call(...args)
};
FontEnd.start('xiaoming');
複製程式碼
上面執行的結果是:
xiaoming get webpack
xiaoming get webpack
xiaoming get webpack
xiaoming get react
複製程式碼
- SyncLoopHook任務能夠執行多次
- 返回undefined則停止執行,返回非undefined則繼續執行當前任務
總結:主要場景是同一任務,需要執行多次
Q10:SyncLoopHook如何實現?
class SyncLoopHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tap(name,task){
this.tasks.push(task);
}
call(...args){
const param = args.slice(0,this.limit.length);
let index = 0;
while(index<this.tasks.length){
const result = this.tasks[index](...param);
if(result === undefined){
index++;
}
}
}
}
複製程式碼
- 上面的實現是通過計數
- 如果結果不為undefined則下標index不移動
- 如果結果為undefined則下標index增加
也可以換doWhile來實現
class SyncLoopHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tap(name,task){
this.tasks.push(task);
}
call(...args){
const param = args.slice(0,this.limit.length);
this.tasks.forEach(task=>{
let ret;
do{
ret = task(...param);
}while(ret!=undefined)
})
}
}
複製程式碼
- 這種實現沒有下標概念了
- 直接遍歷tasks任務組,如果任務組中某一個任務執行的結果不是undefined則再次執行
總結:SyncLoopHook這個使用場景相對較少,不過了解一下也好
Q11:啥是AsyncParralleHook?
前面瞭解的都是同步hook,更關鍵的是非同步hook
舉個例子,同學小王說去學前端了,但你也不知道他什麼時候學完,只有他學完告訴你,你才知道他學完了,例:
const {AsyncParallelHook} = require('tapable');
const FontEnd = new AsyncParallelHook(['name']);
FontEnd.tapAsync('webpack',(name,cb)=>{
setTimeout(() => {
console.log(name+" get webpack ")
cb();
}, 1000);
});
FontEnd.tapAsync('react',(name,cb)=>{
setTimeout(() => {
console.log(name+" get react")
cb();
}, 1000);
});
FontEnd.start=(...args)=>{
FontEnd.callAsync(...args,()=>{
console.log("end");
})
};
FontEnd.start('小王');
複製程式碼
最終輸出:
小王 get webpack
小王 get react
end
複製程式碼
- AsyncParralleHook是非同步並行鉤子
- 使用場景,例如同時發起對兩個介面的請求
- 注意:這次註冊事件,不再是tap了,而是tapAsync
- 注意:這次的事件執行,不再是call了,而是callAsync
- 可以看出tapable中區分了同步、非同步的訂閱和釋出
- 注意:想要讓所有非同步執行完成後,接收到通知,需要執行cb()
Q12:AsyncParralleHook如何實現?
class AsyncParallelHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapAsync(name,task){
this.tasks.push(task);
}
callAsync(...args){
const finalCallBack = args.pop();
const param = args.slice(0,this.limit.length);
let index = 0;
const done=()=>{
index++;
if(index === this.tasks.length){
finalCallBack();
}
}
this.tasks.forEach(item=>item(...param,done))
}
}
複製程式碼
- AsyncParallelHook最簡單就是通過計數
- 在例項上新增一個計數器
- 然後遍歷tasks,當任務成功個數與任務總數相同時,執行finalCallBack
總結:AsyncParallelHook解決的問題和promise.all類似,都是用於解決非同步並行的問題
Q13:AsyncParralleHook(2)如何使用promise?
前面雖然用:AsyncParralleHook能夠解決非同步,但並沒有使用primise,也沒有類promise的概念
const {AsyncParallelHook} = require('tapable');
const FontEnd = new AsyncParallelHook(['name']);
FontEnd.tapPromise('webpack',(name)=>{
return new Promise((resolve)=>{
setTimeout(() => {
console.log(name+" get webpack ")
resolve();
}, 1000);
})
});
FontEnd.tapPromise('react',(name,cb)=>{
return new Promise((resolve)=>{
setTimeout(() => {
console.log(name+" get react ")
resolve();
}, 1000);
})
});
FontEnd.start=(...args)=>{
FontEnd.promise(...args).then(()=>{
console.log("end");
})
};
FontEnd.start('小王');
複製程式碼
呼叫上面的api後,輸出:
小王 get webpack
小王 get react
end
複製程式碼
- 注意:此時繫結事件的方法叫做tapPromise
- 注意:此時執行事件的方法叫做promise
總結:
- tapable共有三種事件繫結方法:tap、tapAsync、tapPromise
- tapable共有三種事件執行方法:call、callAsync、promise
Q14:AsyncParralleHook(2)promise版如何實現?
class AsyncParallelHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapPromise(name,task){
this.tasks.push(task);
}
promise(...args){
const param = args.slice(0,this.limit.length);
const tasks = this.tasks.map(task=>task(...param));
return Promise.all(tasks)
}
}
複製程式碼
- 核心就是實現兩個方法,tapPromise和promise
- tapPromise其實和之前的tap沒有明顯區別(簡單實現的問題)
- promise的話,其實就是返回一個Promise.all
Q15:啥是AsyncParallelBailHook?
AsyncParallelBailHook這個鉤子和前面的鉤子不太一樣 按前面的例子來講:
- 同學小王說去學前端了,但你也不知道他什麼時候學完,只有他學完告訴你,你才知道他學完了
- 小王學了webpack,學崩了,告訴了你
- 你聽說小王學崩了,你就以為他學不下去了,你就對大傢伙說,小王學崩了
- 但是小王同時也學了react卻咬牙學完了
- 雖然學完了,但你已經對外宣佈小王崩了,很打臉,所以就當不知道了
這就是AsyncParallelBailHook處理的事情
const {AsyncParallelBailHook} = require('tapable');
const FontEnd = new AsyncParallelBailHook(['name']);
FontEnd.tapPromise('webpack',(name)=>{
return new Promise((resolve,reject)=>{
setTimeout(() => {
console.log(name+" get webpack ")
reject('小王學崩了!');
}, 1000);
})
});
FontEnd.tapPromise('react',(name,cb)=>{
return new Promise((resolve)=>{
setTimeout(() => {
console.log(name+" get react ")
resolve();
}, 2000);
})
});
FontEnd.start=(...args)=>{
FontEnd.promise(...args).then(()=>{
console.log("end");
},(err)=>{
console.log("聽說:",err)
})
};
FontEnd.start('小王');
複製程式碼
上面程式碼執行結果是:
小王 get webpack
聽說: 小王學崩了!
小王 get react
複製程式碼
- 上面例子,第一個並行任務返回了reject
- reject只要不是undefined,就會直接進入promise.all的catch
- 非同步任務,react還是會執行,但成功後沒有處理了
再看一個例子:
const {AsyncParallelBailHook} = require('tapable');
const FontEnd = new AsyncParallelBailHook(['name']);
FontEnd.tapPromise('webpack',(name)=>{
return new Promise((resolve,reject)=>{
setTimeout(() => {
console.log(name+" get webpack ")
reject();
}, 1000);
})
});
FontEnd.tapPromise('react',(name,cb)=>{
return new Promise((resolve)=>{
setTimeout(() => {
console.log(name+" get react ")
resolve();
}, 2000);
})
});
FontEnd.start=(...args)=>{
FontEnd.promise(...args).then(()=>{
console.log("end");
},(err)=>{
console.log("聽說:",err)
})
};
FontEnd.start('小王');
複製程式碼
和上面就改了1行,就是reject內容為空,此時輸出:
小王 get webpack
小王 get react
end
複製程式碼
- 此時即便呼叫了reject也不會進入到catch
- reject返回空,後面的任務也會照常執行
總結:
- AsyncParallelBailHook,如果返回真值,則直接會走進catch
- 無論返回結果是什麼,所有任務都會執行
- 主要場景是,並行請求3個介面,隨便哪一個返回結果都行,只要返回了,就對返回進行處理(走catch)
- 如果用來處理同步,則和SyncBailHook效果一樣
- 如果處理tapSync,則遇到return true最終的callback不會執行
- 如果處理promise,則遇到rejcet(true),則直接進入catch
Q16:AsyncParallelBailHook如何實現?
這個AsyncParallelBailHook真真燒腦了好一會
class AsyncParallelBailHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapPromise(name,task){
this.tasks.push(task);
}
promise(...args){
const param = args.slice(0,this.limit.length);
const tasks = this.tasks.map(task=>{
return new Promise((resolve,reject)=>{
task(...param).then((data)=>{
resolve(data);
},(err)=>{
err? reject(err):resolve();
});
})
});
return Promise.all(tasks)
}
}
複製程式碼
- 正常情況下,promise.all中任意一個任務reject,就會進入統一的catch
- 但我們需要的是根據reject的值來判斷是否走如catch
- 所以我們在原有task外,再包一層promise
- 如果reject值為真,則執行reject
- 如果reject值為假,則執行resolve,就當什麼也沒發生
Q17:啥是AsyncSeriesHook?
前面講的是非同步並行,現在該說非同步序列了,例如小王,學完webpack才去學的react,你也不知道他什麼時候學完,但他學完一個就會告訴你一下,例:
const {AsyncSeriesHook} = require('tapable');
const FontEnd = new AsyncSeriesHook(['name']);
console.time('webpack');
console.time('react');
FontEnd.tapPromise('webpack',(name,cb)=>{
return new Promise((resolve,reject)=>{
setTimeout(() => {
console.log(name+" get webpack ")
console.timeEnd('webpack');
resolve();
}, 1000);
})
});
FontEnd.tapPromise('react',(name,cb)=>{
return new Promise((resolve)=>{
setTimeout(() => {
console.log(name+" get react ")
console.timeEnd('react');
resolve();
}, 1000);
})
});
FontEnd.start=(...args)=>{
FontEnd.promise(...args).then(()=>{
console.log("end");
})
};
FontEnd.start('小王');
複製程式碼
上面程式碼執行結果:
小王 get webpack
webpack: 1010.781ms
小王 get react
react: 2016.598ms
end
複製程式碼
- 兩個非同步任務,變成了序列
- 從時間能夠得出,兩個1s的非同步的任務,序列後總時間變成了2s
總結:AsyncSeriesHook解決的問題是非同步序列,例如node的os.cpus()有限,可以把任務分批次執行,這樣對效能有保障
Q18:AsyncSeriesHook如何實現?
class AsyncSeriesHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapPromise(name,task){
this.tasks.push(task);
}
promise(...args){
const param = args.slice(0,this.limit.length);
const [first,...others] = this.tasks;
return others.reduce((pre,next)=>{
return pre.then(()=>next(...param))
},first(...param))
}
}
複製程式碼
- 實現核心就是promise序列
- 取出第一個任務,執行拿到promise例項,然後通過reduce遍歷
Q19:啥是AsyncSeriesBailHook?
還是前面的例子,如果小王學前端,學了webapck就徹底放棄了,那後面的react也就不用學了
const {AsyncSeriesBailHook} = require('tapable');
const FontEnd = new AsyncSeriesBailHook(['name']);
console.time('webpack');
console.time('react');
FontEnd.tapPromise('webpack',(name,cb)=>{
return new Promise((resolve,reject)=>{
setTimeout(() => {
console.log(name+" get webpack ")
console.timeEnd('webpack');
reject('小王徹底放棄了');
}, 1000);
})
});
FontEnd.tapPromise('react',(name,cb)=>{
return new Promise((resolve)=>{
setTimeout(() => {
console.log(name+" get react ")
console.timeEnd('react');
resolve();
}, 1000);
})
});
FontEnd.start=(...args)=>{
FontEnd.promise(...args).then(()=>{
console.log("end");
}).catch((err)=>{
console.log("err",err)
})
};
FontEnd.start('小王');
複製程式碼
上面程式碼輸出:
小王 get webpack
webpack: 1010.518ms
err 小王徹底放棄了
複製程式碼
- 上面的程式碼只執行到webpack
- AsyncSeriesBailHook,任務如果return,或者reject,則阻塞了
場景:主要是非同步序列,如果某一個任務執行的結果reject或者return,那麼後面的都將不再執行
Q20:AsyncSeriesBailHook如何實現?
class AsyncSeriesBailHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapPromise(name,task){
this.tasks.push(task);
}
promise(...args){
const param = args.slice(0,this.limit.length);
const [first,...others] = this.tasks;
return new Promise((resolve,reject)=>{
others.reduce((pre,next,index,arr)=>{
return pre
.then(()=>next(...param))
.catch((err=>{
arr.splice(index,arr.length-index);
reject(err);
})).then(()=>{
(index+1 === arr.length) && resolve();
})
},first(...param))
})
}
}
複製程式碼
AsyncSeriesBailHook實現難度要高很多
- 首先在reduce外再包一層promise
- 當遇到任何一個子任務進入catch的時候,則將reduce的第四個引數arr切割,使其無法再向下進行,也就是停止reduce的繼續
- 同時所有promise後面再新增一個後置then,用來檢測是否全部執行完成
- 為什麼使用index+1,是因為後置then肯定是最後一個任務,但遍歷index還處於上一個下標,所以只要加1就好
Q21:啥是AsyncSeriesWaterfallHook?
SyncWaterFallHook前面已經瞭解過了,就是前一個執行完的結果會傳遞給下一個執行函式,和AsyncSeriesWaterfallHook的區別就是,一個是同步一個是非同步
具體來說,例如只有一本教材,小王學完,小張才能學
const FontEnd = new AsyncSeriesWaterfallHook(['name']);
FontEnd.tapAsync('webpack',(name,cb)=>{
setTimeout(() => {
console.log(name+" get webpack ")
cb(null,'小李');
}, 1000);
});
FontEnd.tapAsync('webpack',(name,cb)=>{
setTimeout(() => {
console.log(name+" get webpack ")
cb(null,'小張');
}, 1000);
});
FontEnd.tapAsync('webpack',(name,cb)=>{
setTimeout(() => {
console.log(name+" get webpack ")
cb(null,'小紅');
}, 1000);
});
FontEnd.start=(...args)=>{
FontEnd.callAsync(...args,(data)=>{
console.log("全學完了",)
})
};
FontEnd.start('小王');
複製程式碼
上面程式碼,最終輸出:
小王 get webpack
小李 get webpack
小張 get webpack
全學完了
複製程式碼
總結:這個的用法和SyncWaterFallHook的用法一致
Q22:AsyncSeriesWaterfallHook如何實現?
class AsyncSeriesWaterfallHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapAsync(name,task){
this.tasks.push(task);
}
callAsync(...args){
const param = args.slice(0,this.limit.length);
const finalCallBack = args.pop();
let index = 0;
const next = (err,data)=>{
const task = this.tasks[index];
if(!task)return finalCallBack();
if(index === 0){
task(...param,next)
}else{
task(data,next)
}
index++;
}
next();
}
}
複製程式碼
- 主要是通過封裝一個回撥函式next
- 然後不斷呼叫任務佇列中的任務,呼叫的時候,再傳遞相同的回撥函式進去
prmise版本的實現如下:
class AsyncSeriesWaterfallHook {
constructor(limit = []){
this.limit= limit;
this.tasks = [];
}
tapPromise(name,task){
this.tasks.push(task);
}
promise(...args){
const param = args.slice(0,this.limit.length);
const [first,...others] = this.tasks;
return others.reduce((pre,next)=>{
return pre.then((data)=>{
return data?next(data):next(...param);
})
},first(...param))
}
}
複製程式碼
- promise的實現要相對簡單一些
- 主要去看then方法中是否有內容,如果有的話,則傳遞個下一個函式,如果沒有,則用初始引數
總結
- tapable的各AsyncHook都同時支援tap、tapAsync、tapPromise
- tapable主要解決的是事件流轉的問題,各個Hook使用的場景各有不同
- tapable主要應用在webpack的各個生命週期中,具體的實踐還需要結合webpack原理去看