JavaScript fetch,失败时重试。简介 简单思考一下 fetch 怎么实现?总结

2025-06-07

Javascript 获取,失败时重试。

介绍

让我们思考一下

简要介绍 fetch

那么如何实现呢?

结论

由于某种原因,之前的帖子没有出现在我的个人资料中,因此重新发布了此帖子。

最近,我遇到了网络随机通断的情况。由于这会影响测试结果的一致性,我决定实现一个在网络故障时最多fetch_retry重试 100 次的功能fetchn

介绍

JavaScript 中的Fetch非常棒。我希望你会同意,它提供了一个简单但足够强大的接口来处理我们的 AJAX 请求。

然而,网络并不总是像我们期望的那样工作,它可能会随机失败。为了解决这个问题,让我们实现一个函数,fetch_retry(url, options, n)该函数会在失败时fetch(url, options)重试最多 100n次。从而提高成功的几率。

让我们思考一下

重试听起来像个循环。我们为什么不写一个 for/while 循环来实现呢?或许可以像下面这样?

function fetch_retry(url, options, n) {
    for(let i = 0; i < n; i++){
        fetch(url, options);
        if(succeed) return result;
    }
}
Enter fullscreen mode Exit fullscreen mode

不! Fetch 是一个异步函数,这意味着程序不会等待结果就继续执行!n无论之前的调用是否成功,fetch 都会被同时调用(某种程度上来说)。

这不是我们想要的。这不是失败重试,而是n同时获取时间!(话虽如此,如果写得正确,它也可以增加成功的几率。也许可以用类似这样的东西Promsie.any?虽然我不是 Bluebird 的忠实粉丝。我认为原生的 Promise 已经足够好了。)

如果您不了解异步函数和Javascript,在​​继续阅读之前观看由Jessica Kerr制作的Promise精彩视频!

简要介绍 fetch

所以fetch返回一个Promise。我们通常这样称呼它。

fetch(url, { method: 'GET' }).then(res => console.log('done'));
console.log('fetching...');
Enter fullscreen mode Exit fullscreen mode

如果你理解Promise正确的话,你应该期望结果是:

fetching...
done
Enter fullscreen mode Exit fullscreen mode

如果网络由于某种原因出现故障,Promise则会拒绝,我们可以按如下方式捕获错误:

fetch(url, { method: 'GET' }).catch(err => /* ... */);
Enter fullscreen mode Exit fullscreen mode

那么如何实现呢?

做什么fetch_retry

我们首先思考一下这个函数到底想做什么fetch_retry。我们知道它必须以某种方式调用 fetch,所以就把它写下来。

function fetch_retry(url, options, n) {
    fetch(url, options)
        .then(function(result) {
            /* on success */
        }).catch(function(error) {
            /* on failure */
        })
}
Enter fullscreen mode Exit fullscreen mode

现在显然fetch_retry必须是一个异步函数,因为我们无法真正从异步函数中定义一个同步函数。(或者我们可以吗?启发我。)

定义:这意味着如果任何一次尝试成功,fetch_retry则返回一个解析,如果所有尝试都失败,则返回一个拒绝。Promisenn

那么现在让我们返回Promise

function fetch_retry(url, options, n) {
    return new Promise(function(resolve, reject) { // <--- we know it is asynchronous, so just return a promise first!
        fetch(url, options)
            .then(function(result) {
                /* on success */
            }).catch(function(error) {
                /* on failure */
            })
    });
}
Enter fullscreen mode Exit fullscreen mode

万一fetch成功了呢?

因此,如果获取成功,我们显然可以通过调用该resolve函数来解析返回的 Promise。因此代码如下:

function fetch_retry(url, options, n) {
    return new Promise(function(resolve, reject) {
        fetch(url, options)
            .then(function(result) {
                /* on success */
                resolve(result); // <--- yeah! we are done!
            }).catch(function(error) {
                /* on failure */
            })
    });
}
Enter fullscreen mode Exit fullscreen mode

如果fetch失败了怎么办?

失败时该怎么办?由于我们之前讨论过的异步特性,在这里使用 for/while 循环实际上没什么用。但有一件事我们可以做到 for/while 循环能做到的。是不是想起来了?没错!递归!

执行递归时我的两个经验法则:

  1. 不要用递归的方式思考。不要尝试用递归的方式追踪你的代码。
  2. 凭着信念的飞跃,假设你定义的递归函数有效。

这两点本质上是一样的!如果你有信心,你就不会在代码中递归地思考了。

好的,让我们尝试一下信念的飞跃,并假设fetch_retry它会神奇地起作用。

如果它有效,那么在中on failure,如果我们调用会发生什么fetch_retry(url, options, n - 1)

function fetch_retry(url, options, n) {
    return new Promise(function(resolve, reject) {
        fetch(url, options)
            .then(function(result) {
                /* on success */
                resolve(result);
            })
            .catch(function(error) {
                /* on failure */
                fetch_retry(url, options, n - 1) // <--- leap of faith, this will just work magically! Don't worry!
                    .then(/* one of the remaining (n - 1) fetch succeed */)
                    .catch(/* remaining (n - 1) fetch failed */);
            })
    });
}
Enter fullscreen mode Exit fullscreen mode

fetch_retry(url, options, n - 1)只需凭着信念的飞跃就能奇迹般地发挥作用,并返回一个Promise,根据我们之前讨论过的定义,如果任何尝试(在n - 1尝试中)成功则解析,如果所有n - 1尝试都失败则拒绝。

那么现在,递归调用之后我们该做什么呢?注意,因为fetch_retry(url, options, n - 1)会神奇地工作,这意味着我们n此时已经完成了所有的获取操作。在这种on failure情况下,如果fetch_retry(url, options, n - 1)解析成功,就解析;如果拒绝,就拒绝。

function fetch_retry(url, options, n) {
    return new Promise(function(resolve, reject) {
        fetch(url, options)
            .then(function(result) {
                /* on success */
                resolve(result);
            })
            .catch(function(error) {
                fetch_retry(url, options, n - 1)
                    .then(resolve)  // <--- simply resolve
                    .catch(reject); // <--- simply reject
            })
    });
}
Enter fullscreen mode Exit fullscreen mode

太棒了!快完成了!我们知道这个递归调用需要一个起始条件。在考虑起始条件时,我们会查看函数参数,并确定在什么情况下可以立即得出结果。

答案是当n === 1fetch失败时。在这种情况下,我们可以直接拒绝 的错误fetch,而无需fetch_retry递归调用。

function fetch_retry(url, options, n) {
    return new Promise(function(resolve, reject) {
        fetch(url, options)
            .then(function(result) {
                /* on success */
                resolve(result);
            })
            .catch(function(error) {
                if (n === 1) return reject(error); // <--- base case!
                fetch_retry(url, options, n - 1)
                    .then(resolve)
                    .catch(reject);
            })
    });
}
Enter fullscreen mode Exit fullscreen mode

清理干净

冗余功能

在我们的“成功时”函数中,我们只是调用了resolve(result)。所以这个函数实例是多余的,我们可以直接将其用作resolve“成功时”函数。因此代码将变为:

function fetch_retry(url, options, n) {
    return new Promise(function(resolve, reject) {
        fetch(url, options).then(resolve) // <--- Much cleaner!
            .catch(function(error) {
                if (n === 1) return reject(error);
                fetch_retry(url, options, n - 1)
                    .then(resolve)
                    .catch(reject);
            })
    });
}
Enter fullscreen mode Exit fullscreen mode

多余的承诺

现在我们在这里做的另一件愚蠢的事情是这一行:

fetch_retry(url, options, n - 1).then(resolve).catch(reject)
Enter fullscreen mode Exit fullscreen mode

你知道问题是什么吗?

让我来解释一下,我们基本上是这样做的:

new Promise(function(resolve, reject) {
    fetch_retry(url, options, n - 1).then(resolve).catch(reject)
});
Enter fullscreen mode Exit fullscreen mode

所以在这种情况下,这个新的 Promise 是多余的,因为如果fetch_retryresolve 则解析,如果fetch_retryrejection 则拒绝。所以它的行为基本上和 的行为完全一样fetch_retry

因此,上述代码在语义上与单独执行基本相同fetch_retry

fetch_retry(url, options, n - 1)
// sementically the same thing as the following
new Promise(function(resolve, reject) {
    fetch_retry(url, options, n - 1).then(resolve).catch(reject)
});
Enter fullscreen mode Exit fullscreen mode

为了简化代码,还需要掌握一些知识。我们可以promise.then用下面的方式链接 s 。因为它promise.then也返回一个 Promise!

Promise.resolve(3).then(function(i) {
    return i * 2;
}).then(function(i) {
    return i === 6; // this will be true
});
Enter fullscreen mode Exit fullscreen mode

如你所见,我们可以将处理后的值传递给下一个,then依此类推。如果值为 a Promise,则下一个then将接收返回的Promise解析结果。如下所示:

Promise.resolve(3).then(function(i) {
    return i * 2;
}).then(function(i) {
    return Promise.resolve(i * 2); // also work!
}).then(function(i) {
    return i === 12; // this is true! i is not a Promise!
};
Enter fullscreen mode Exit fullscreen mode

catch同样的想法也可以应用!感谢Corentin的鼓励!这意味着我们甚至可以在 Promise 被拒绝时,将其解决。以下是一个例子:

Promise.resolve(3).then(function(i) {
    throw "something's not right";
}).catch(function(i) {
    return i
}).then(function(i) {
    return i === "something's not right";
};
Enter fullscreen mode Exit fullscreen mode

那么,我们该如何利用这些知识进行清理呢?我们的代码似乎更复杂。

function fetch_retry(url, options, n) {
    return new Promise(function(resolve, reject) {
        fetch(url, options).then(resolve)
            .catch(function(error) {
                if (n === 1) return reject(error);
                fetch_retry(url, options, n - 1)
                    .then(resolve)  // <--- we try to remove this
                    .catch(reject); // <--- and this
            })
    });
}
Enter fullscreen mode Exit fullscreen mode

好吧,我们可以用 ! 返回的 Promise 来解析返回的 Promise fetch_retry,而不是 。fetch_retry(...).then(resolve).catch(reject)我们可以这样做resolve(fetch_retry(...))! 因此代码变为:

function fetch_retry(url, options, n) {
    return new Promise(function(resolve, reject) {
        fetch(url, options).then(resolve)
            .catch(function(error) {
                if (n === 1) return reject(error);
                resolve(fetch_retry(url, options, n - 1)); // <--- clean, isn't it?
            })
    });
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以更进一步,Promise通过解决中的承诺来消除明确的创建catch

function fetch_retry(url, options, n) {
    return fetch(url, options).catch(function(error) {
        if (n === 1) throw error;
        return fetch_retry(url, options, n - 1);
    });
}
Enter fullscreen mode Exit fullscreen mode

引用MDN中的一些内容,并对其进行了一些调整,使其更通俗易懂:

如果处理程序抛出错误或返回本身已被拒绝的 Promise,则 catch(handler) 返回的 Promise 将被拒绝;否则,它将被解决。

ES6

我可以预料到,一些 JS 大佬会因为我不使用箭头函数而讨厌我。我不会为那些不习惯使用箭头函数的人使用。这是用箭头函数编写的 ES6 版本,我就不多解释了。

const fetch_retry = (url, options, n) => fetch(url, options).catch(function(error) {
    if (n === 1) throw error;
    return fetch_retry(url, options, n - 1);
});
Enter fullscreen mode Exit fullscreen mode

快乐的?

ES7

是啊,PromiseES7 的 async/await 版本发布后,它很快就会过时。所以这里有一个 async/await 版本:

const fetch_retry = async (url, options, n) => {
    try {
        return await fetch(url, options)
    } catch(err) {
        if (n === 1) throw err;
        return await fetch_retry(url, options, n - 1);
    }
};
Enter fullscreen mode Exit fullscreen mode

看起来整洁多了,对吧?

事实上,我们不必在 ES7 中使用递归,我们可以使用简单的 for 循环来定义它。

const fetch_retry = async (url, options, n) => {
    let error;
    for (let i = 0; i < n; i++) {
        try {
            return await fetch(url, options);
        } catch (err) {
            error = err;
        }
    }
    throw error;
};

// or (tell me which one u like better, I can't decide.)

const fetch_retry = async (url, options, n) => {
    for (let i = 0; i < n; i++) {
        try {
            return await fetch(url, options);
        } catch (err) {
            const isLastAttempt = i + 1 === n;
            if (isLastAttempt) throw err;
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

结论

总结一下,我们研究了同一个函数的四个不同版本。其中三个是递归的,只是编写风格和风格不同。最后一个使用了 for 循环。让我们回顾一下:

原始版本

function fetch_retry(url, options, n) {
    return fetch(url, options).catch(function(error) {
        if (n === 1) throw error;
        return fetch_retry(url, options, n - 1);
    });
}
Enter fullscreen mode Exit fullscreen mode

ES6

const fetch_retry = (url, options, n) => fetch(url, options).catch(function(error) {
    if (n === 1) throw error;
    return fetch_retry(url, options, n - 1);
});
Enter fullscreen mode Exit fullscreen mode

ES7 async/await 递归

这是我最喜欢的。

const fetch_retry = async (url, options, n) => {
    try {
        return await fetch(url, options)
    } catch(err) {
        if (n === 1) throw err;
        return await fetch_retry(url, options, n - 1);
    }
};
Enter fullscreen mode Exit fullscreen mode

ES7 async/await for 循环

const fetch_retry = async (url, options, n) => {
    let error;
    for (let i = 0; i < n; i++) {
        try {
            return await fetch(url, options);
        } catch (err) {
            error = err;
        }
    }
    throw error;
};

// or (tell me which one u like better, I can't decide.)

const fetch_retry = async (url, options, n) => {
    for (let i = 0; i < n; i++) {
        try {
            return await fetch(url, options);
        } catch (err) {
            const isLastAttempt = i + 1 === n;
            if (isLastAttempt) throw err;
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

在评论中告诉我你的反馈!:D

文章来源:https://dev.to/ycmjason/javascript-fetch-retry-upon-failure-3p6g
PREV
看来 Vue.js 周末可能会超越 React!构建这个项目时值得一提的几点:
NEXT
从浏览器检测条形码!!!