咱們能夠把每次異步操做(Promise或者setTimeout)當作一個單獨的異步線程。在實際的編程過程當中,大多數狀況下咱們對異步線程是不作監管、讓其自生自滅的,但這樣容易引起一些問題。編程
好比某個組件A,其中請求了後端定位解析服務;而當A組件被使用到一個列表中時,列表的for循環會形成n次請求併發。咱們能夠將這種狀況,當作是異步行爲產生了抖動。後端
下面這段僞代碼,描述了咱們平常所進行的防抖處理的基本要點:緩存
const debounce = (func, delay = 500) => {
let timeout = 0;
return (...args) => {
// 若是沒有阻斷
if(!timeout){
// 那麼開始阻斷
timeout = setTimeout(() => {
// delay以後解除阻斷
timeout = 0;
}, delay)
// 當即執行
func(...args);
} else {
// 什麼也不作
}
}
}
複製代碼
能夠看出,這種防抖的主要思路,是把delay
延遲期間的行爲都阻斷。咱們能夠把這當作是一種行爲防抖,在鼠標點擊、鼠標移動等事件綁定場景中常常使用。併發
可是,在異步操做中,好比上面的案例1,for循轉中組件的異步請求行爲若是被阻斷,那每一個組件都所須要的數據也就拿不到,也沒法經過resolve或reject觸發下一步邏輯。這種狀況下,咱們須要作數據防抖。dom
數據防抖的流程,大體分如下幾步:異步
簡單講,就是把一組異步請求,都交給第一發請求來作,剩餘的僅等待請求結果。這有點相似進程和線程的關係。同時,由於不一樣http請求的url不同,其返回數據也是不一樣的,因此須要對每次異步請求按url進行分組,對共享的數據也須要按url進行隔離。測試
首先模擬一個異步請求:ui
let somePromise = (key) => new Promise((resolve, reject) => {
setTimeout(() => {
resolve([key, Math.random()]);
}, 500 + 500 * Math.random())
});
複製代碼
咱們能夠使用參數url
對httpGet請求進行分組。url
下面是基於Promise所作的防抖spa
const debouncePromise = (factory, keyIndex = 0, delay = 50) => {
// 共享數據空間
const cache = {};
return (...args) => new Promise((resolve, reject) => {
// 獲取緩存分組
let key = args[keyIndex];
let state = cache[key];
if(!state){
state = { status: 0, taskCount: 0 };
cache[key] = state;
}
// 首發請求,可看爲主線程
if(state.status === 0){
// 鎖定狀態,掛起其餘請求。
state.status = 1;
factory(...args).then(result => {
// 結束自身異步行爲
resolve(result);
// 共享數據
state.result = result;
// 解鎖狀態,通知其餘請求。
state.status = 2;
}, err => {
reject(err);
state.result = err;
// 解鎖狀態,通知其餘請求。
state.status = 3;
})
}
// 其餘請求,可看爲輔線程,僅等待主線程結果。
else if(state.status === 1){
// 任務數+1
state.taskCount += 1;
const waitingHandle = setInterval(() => {
// 已解鎖狀態,2或3
if(state.status > 1){
// 清理等待循環
clearInterval(waitingHandle);
// 處理結果
(state.status === 2 ? resolve : reject)(state.result);
// 任務數-1
state.taskCount -= 1;
// 若是任務數歸零,說明自身是最後一個線程
// reset狀態和數據
if(state.taskCount <= 0){
delete cache[key];
}
}
}, delay)
}
})
}
複製代碼
測試代碼:
somePromise('aaaaa').then(res => console.log(res))
somePromise('aaaaa').then(res => console.log(res))
somePromise('aaaaa').then(res => console.log(res))
// debounce it
somePromise = debouncePromise(somePromise)
somePromise('bbb').then(res => console.log(res))
somePromise('bbb').then(res => console.log(res))
somePromise('bbb').then(res => console.log(res))
somePromise('cc').then(res => console.log(res))
somePromise('cc').then(res => console.log(res))
somePromise('cc').then(res => console.log(res))
// 低於delay閾值再次推入隊列
// 期待結果應與上面的bbb分組一致。
setTimeout(() => {
somePromise('bbb').then(res => console.log(res))
somePromise('bbb').then(res => console.log(res))
somePromise('bbb').then(res => console.log(res))
}, 10)
// 高於delay閾值從新發起請求
// 期待結果應與上面的bbb分組不一致。
setTimeout(() => {
somePromise('bbb').then(res => console.log(res))
somePromise('bbb').then(res => console.log(res))
somePromise('bbb').then(res => console.log(res))
}, 1000)
複製代碼
執行結果:
// 防抖前,每次請求結果抖動。
["aaaaa", 0.6301757853487]
["aaaaa", 0.2816070377500479]
["aaaaa", 0.009064307010989259]
//防抖後,delay閾值內結果不抖動
["bbb", 0.43005402041935437]
["bbb", 0.43005402041935437]
["bbb", 0.43005402041935437]
["cc", 0.956314414078062]
["cc", 0.956314414078062]
["cc", 0.956314414078062]
// delay閾值內認爲是抖動,保持數據共享
["bbb", 0.43005402041935437]
["bbb", 0.43005402041935437]
["bbb", 0.43005402041935437]
// delay閾值外認爲是新的請求
["bbb", 0.7923457809536392]
["bbb", 0.7923457809536392]
["bbb", 0.7923457809536392]
複製代碼
執行結果符合期待。