今天看啥  ›  专栏  ›  JS老狗

异步的防抖

JS老狗  · 掘金  ·  · 2019-08-22 04:54
阅读 315

异步的防抖

异步抖动

我们可以把每次异步操作(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触发下一步逻辑。这种情况下,我们需要做数据防抖

数据防抖的流程,大致分以下几步:

  1. 在第一次异步请求发起之后,挂起后续所有请求;
  2. 在第一次异步请求返回之后,向所有挂起的请求共享数据
  3. 当所有挂起队列被清空后,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]
复制代码

执行结果符合期待。

示例代码:codepen.io/marvin_2019…




原文地址:访问原文地址
快照地址: 访问文章快照