JavaScript: 使用setTimeout实现重试的函数

5

我有一个名为downloadItem的函数,由于网络原因可能会失败,我希望在最终拒绝该项之前能够重试几次。由于如果存在网络问题,立即重试是没有意义的,因此重试需要设置超时。

以下是我目前的代码:

function downloadItemWithRetryAndTimeout(url, retry, failedReason) {
    return new Promise(function(resolve, reject) {
        try {
            if (retry < 0 && failedReason != null) reject(failedReason);

            downloadItem(url);
            resolve();
        } catch (e) {
            setTimeout(function() {
                downloadItemWithRetryAndTimeout(url, retry - 1, e);
            }, 1000);
        }
    });
}

显然这会失败,因为第二次(及以后)调用downloadItemWithRetryAndTimeout时,我没有像要求的那样返回一个promise。如何让第二个promise正确工作?附带说明,如果有关系,代码正在NodeJS中运行。

也许使用 Promise 并不适合你的场景。我不明白为什么人们突然在每个异步情况下都使用 Promise。 - webduvet
我不一定非得使用 Promise,你有没有不需要 Promise 的解决方案? - AlexD
1
@WebDuvet: 因为这就是Promise的用途:处理具有单一结果的异步情况。它非常适合这种场景。 - Bergi
1
@AlexD:你的downloadItem函数真的是同步的吗?我认为它应该返回一个Promise。 - Bergi
1
@alexd,使用Bluebird,您可以使用Promise.delayPromise.try - aarosil
@Bergi, 对的,但是如果你只需要一次结果,在一个情况下只需要一次,并且没有遇到任何嵌套回调的问题,使用 Promise 库只会增加不必要的复杂性。 - webduvet
4个回答

5

我有两个想法:

将Promise从迭代函数downloadItemWithRetryAndTimeout之外移动-现在resolve()将可用于所有迭代:

function downloadWrapper(url, retry) {
    return new Promise(function (resolve, reject) {
        function downloadItemWithRetryAndTimeout(url, retry, failedReason) {

            try {
                if (retry < 0 && failedReason != null)
                    reject(failedReason);

                downloadItem(url);
                resolve();
            } catch (e) {
                setTimeout(function () {
                    downloadItemWithRetryAndTimeout(url, retry - 1, e);
                }, 1000);
            }

        }

        downloadItemWithRetryAndTimeout(url, retry, null);
    });
}

这个解决方案可以运行,但它是一种反模式,因为它会中断 Promise 链: 由于每次迭代都返回一个 Promise,请只解析该 Promise 并使用 .then 解析前一个 Promise,以此类推:
function downloadItemWithRetryAndTimeout(url, retry, failedReason) {
    return new Promise(function (resolve, reject) {
        try {
            if (retry < 0 && failedReason != null)
                reject(failedReason);

            downloadItem(url);
            resolve();
        } catch (e) {
            setTimeout(function () {
                downloadItemWithRetryAndTimeout(url, retry - 1, e).then(function () {
                    resolve();
                });
            }, 1000);
        }
    });
}

我真的很喜欢你的第一种解决方案!由于在承诺内部声明内部函数后,您还必须调用它一次,因此我对其进行了一些更改。我现在会尝试它。 - AlexD
我太粗心了 - 忘记运行函数了 - 已修复。 - Ori Drori
1
请避免使用promise constructor antipattern - Bergi
@Bergi - 谢谢。没想到它会陷入同样的延迟陷阱。 - Ori Drori

3

不需要创建新的Promise来处理这个问题。假设downloadItem是同步的并返回一个Promise,只需返回调用它的结果,并使用catch来递归调用downloadItemWithRetryAndTimeout

function wait(n) { return new Promise(resolve => setTimeout(resolve, n)); }

function downloadItemWithRetryAndTimeout(url, retry) {
  if (retry < 0) return Promise.reject();

  return downloadItem(url) . 
    catch(() => wait(1000) . 
      then(() => downloadItemWithRetryAndTimeout(url, retry - 1)
  );
}

以下可能会更加简洁:

function downloadItemWithRetryAndTimeout(url, retry) {
  return function download() {
    return --retry < 0 ? Promise.reject() :
      downloadItem(url) . catch(() => wait(1000) . then(download));
  }();
}

2
最好通过一个通用的重试方法 const retry = (fn, retries = 3) => fn().catch(e => retries <= 0? Promise.reject(e) : retry(fn, retries - 1)),并将其与超时时间进行组合:const delay = ms => new Promise(r => setTimeout(r, ms))const delayError = (fn, ms) => fn().catch(e => delay(ms).then(y => Promise.reject(e)))。然后代码变成 const downloadWithRetryAndTimeout = retry(delayError(download, 1000)) 或类似的方式。 - Benjamin Gruenbaum
@BenjaminGruenbaum 感谢您提供这个优雅的分解。 - user663031

2

@BenjaminGruenbaum对@user663031的评论非常棒,但有一个小错误,原因是:

const delayError = (fn, ms) => fn().catch(e => delay(ms).then(y => Promise.reject(e)))

应该实际是:

const delayError = (fn, ms) => () => fn().catch(e => delay(ms).then(y => Promise.reject(e)))

因此,它将返回一个函数,而不是一个承诺。这是一个棘手的错误,很难解决,所以我在这里发布,以防有人需要。以下是整个内容:

const retry = (fn, retries = 3) => fn().catch(e => retries <= 0 ? Promise.reject(e) : retry(fn, retries - 1))
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
const delayError = (fn, ms) => () => fn().catch(e => delay(ms).then(y => Promise.reject(e)))
retry(delayError(download, 1000))

1
function downloadItemWithRetryAndTimeout(url, retry) {
    return new Promise(function(resolve, reject) {
        var tryDownload = function(attempts) {
            try {
                downloadItem(url);
                resolve();
            } catch (e) {
                if (attempts == 0)  {
                    reject(e);
                } else {
                    setTimeout(function() {
                        tryDownload(attempts - 1);
                    }, 1000);
                }
            }
        };
        tryDownload(retry);
    });
}

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