异步抖动
我们可以把每次异步操作(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触发下一步逻辑。这种情况下,我们需要做数据防抖。
数据防抖的流程,大致分以下几步:
- 在第一次异步请求发起之后,挂起后续所有请求;
- 在第一次异步请求返回之后,向所有挂起的请求共享数据;
- 当所有挂起队列被清空后,Reset状态和数据。
简单讲,就是把一组异步请求,都交给第一发请求来做,剩余的仅等待请求结果。这有点类似进程和线程的关系。同时,因为不同http请求的url不一样,其返回数据也是不同的,所以需要对每次异步请求按url进行分组,对共享的数据也需要按url进行隔离。
简单的实现
首先模拟一个异步请求:
let somePromise = (key) => new Promise((resolve, reject) => {
setTimeout(() => {
resolve([key, Math.random()]);
}, 500 + 500 * Math.random())
});
复制代码
我们可以使用参数url
对httpGet请求进行分组。
下面是基于Promise所做的防抖
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]
复制代码
执行结果符合期待。