我的去抖axios请求实现导致promise永远处于待定状态,是否有更好的方法?

8

我需要一个简单的去抖函数,始终为true。
不使用lodash,并借助于Can someone explain the "debounce" function in Javascript,我将其实现如下:

function debounce(func, wait) {
    var timeout;
    return function() {
        if (!timeout) func.apply(this, arguments);
        clearTimeout(timeout);
        timeout = setTimeout(()=>{timeout = null}, wait);
    };
};

在我需要防抖 axios 请求时,它按预期工作。假设我有一个防抖的 axios 方法,我希望调用方法像往常一样,也就是说,我的防抖 axios 方法应该返回 Promise。

   //the calling method should not change   
   debounced_axios().then(res => {...}).catch(err => {...}) 

原始的防抖实现的本质是在等待时间内只运行一次函数,但我如何在等待时间内返回一个promise?
然后我想到了以下解决方案。
all_timers = {}
function debounce_axios(input, wait) {
    return new Promise((resolve, reject) => {
        let timer = all_timers.[input] //check if it is a repeated request, pseudo code
        if (!timer) {
            axios(input).then(res=>{
                resolve(res)
            }).catch(err => {
                reject(err)
            })
        }
        clearTimeout(timer);
        timer = setTimeout(()=>{timer = null}, wait);
        all_timers[input] = timer
    };
};

我的debounce_axios的核心是让promise在重复请求时保持挂起状态,因此调用方法debounced_axios().then(res => {...}).catch(err => {...})不需要更改。
这里的答案“JavaScript永久挂起的promise有副作用吗?”说“不应该有任何副作用。”
但我仍然不确定让promise永久挂起是否100%安全。
另一个问题是Promise Anti patterns建议不创建不必要的promise。但在我的情况下,创建一个新的promise似乎是必要的。
简而言之,在axios请求(或任何返回promise的请求)中有没有一种简单的方式来防抖?

“*.then(res => {...}).catch(err => {...})*” 究竟是什么意思?为什么不直接将其放在创建 debounced_axios 的函数内部? - Bergi
各种事件处理程序用于操作DOM,因此我无法将它们输入到防抖动axios中。 - Qiulang
也许你不应该再称它为 debounced_axious,但为什么不彻底防抖整个东西呢? - Bergi
是的,我同意防抖UI可能是一个更好的想法。但事实上,这是一个具有相当大的代码库的SPA应用程序,太多地方需要修改,尽管我们将axios方法包装在一个实用类中,所以很容易只修改它。顺便说一下,我是从你那里得到这个想法的。谢谢。 - Qiulang
1
如果您不喜欢永远挂起的承诺,那么立即拒绝一个过早发出的请求是否可以被接受? - trincot
@trincot 我认为这可能是另一个选项。不幸的是,在我的情况下,我正在处理一个旧的代码库,如果我使用那个,许多地方需要进行修改,即在catch块中添加处理抖动拒绝的代码。这是我提出永久挂起承诺的另一个原因,这样我就可以尽可能少地触碰旧代码。 - Qiulang
2个回答

4
但我仍然不确定是否应该让承诺永远保持在未决状态。我同意这并不是一个好主意。更好的方法是将整个Promise链移动到防抖函数内部。另一个选项是在防抖调用未触发新请求时返回缓存值。这将解决您总是需要返回Promise的问题。
function debounce(func, wait) {
    var timeout, value;
    return function() {
        if (!timeout) value = func.apply(this, arguments);
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            timeout = value = null;
        }, wait);
        return value;
    };
}

当你的请求完成时,这意味着有时候会调用多个then处理程序。这取决于你的应用程序是否存在问题或者只是多余的工作。

另一个问题是Promise反模式建议不要创建不必要的Promise,但在我的情况下,创建一个新的Promise似乎是必要的。

只需要一个Promise:当你创建那个永远不会被解决的Promise时就好了。你可以这样写:

function debounce(func, wait) {
    var timeout;
    const never = new Promise(resolve => {/* do nothing*/});
    return function() {
        const result = timeout ? never : func.apply(this, arguments);
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            timeout = null;
        }, wait);
        return result;
    };
}

或者至少避免使用 .then(resolve).catch(reject) 这段代码,最好写成:

function debounce(func, wait) {
    var timeout;
    return function() {
        return new Promise(resolve => {
            if (!timeout) resolve(func.apply(this, arguments));
//                        ^^^^^^^
            clearTimeout(timeout);
            timeout = setTimeout(() => {
                timeout = null;
            }, wait);
        });
    };
}

如果您考虑在超时尚未发生的情况下拒绝承诺(以便调用代码可以处理拒绝),则不需要使用new Promise

function debounce(func, wait) {
    var timeout;
    return function() {
        const result = timeout
          ? Promise.reject(new Error("called during debounce period"))
          : Promise.resolve(func.apply(this, arguments));
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            timeout = null;
        }, wait);
        return result;
    };
}

嗨,我正在验证您的每个建议。关于返回缓存值,我不确定它是否有效。因为我假设在调用方法的承诺链中,缓存的承诺将被调用多次,对吗? - Qiulang
我以前从未缓存过 Promise,所以我想这是一个新手问题。因此,缓存 Promise 并重复使用它是安全的吗?使用缓存的 Promise 运行 Promise 链是否安全? - Qiulang
是的,缓存 Promise 是完全正常并且运行良好的。Promise 不会被“调用”,它是一个结果值 - 请求只会被执行一次,但响应将传递给所有使用 .then() 注册的处理程序。 - Bergi
嗨,我仍在验证您的答案:$。对于“或者至少避免.then(resolve).catch(reject)部分。”我认为您会返回一个永远挂起的承诺来进行防抖调用,就像创建从未解决的承诺一样。我们回到了原点,也就是我的最初问题,这样做好吗?难道不是这种情况吗? - Qiulang
1
@Qiulang 是的,我认为至少有三种更好的方法,但如果你仍然想要这样做,那么至少要做到正确(如第二或第三个片段所示)。 - Bergi

1

实质上,您需要分享防抖函数的结果。在您的情况下,这是一个promise:

const debouncedGetData = debounce(getData, 500)
let promiseCount = 0
let resultCount = 0
test()

function test() {
  console.log('start')
  callDebouncedThreeTimes()
  setTimeout(callDebouncedThreeTimes, 200)
  setTimeout(callDebouncedThreeTimes, 900)
}

function callDebouncedThreeTimes () {
   for (let i=0; i<3; i++) {
      debouncedGetData().then(r => {
        console.log('Result count:', ++resultCount)
        console.log('r', r)
      })
   }
}

function debounce(func, wait) {
    let waiting;
    let sharedResult;
    return function() {
        // first call will create the promise|value here
        if (!waiting) {
          setTimeout(clearWait, wait)
          waiting = true
          sharedResult = func.apply(this, arguments);
        }
        // else new calls within waitTime will be discarded but shared the result from first call

        function clearWait() {
          waiting = null
          sharedResult = null
        }

        return sharedResult
    };
}

function getData () {
  console.log('Promise count:', ++promiseCount)
  return new Promise((resolve, reject) => {
    setTimeout(() => {
       resolve(666)
    }, 1000)
  })
}

1
我认为那不正确,因为(a)正常的去抖方法应该返回经过去抖处理的方法 (b) 在axios的情况下,当去抖的情况返回空值时,调用的方法将会获得UnhandledPromiseRejectionWarning: TypeError:(...).then不是一个函数。 - Qiulang
我相信这里的防抖函数可以用于任何类型的函数,因为它不依赖于 promises。 - Doğancan Arabacı
谢谢,我会验证并回复您。 - Qiulang

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接