在前端实现一个异步任务失败重试的方法,同时接收允许重试的次数,这是一个非常常见的需求,特别是在处理网络请求时。
以下是一个使用 async/await
和 Promise
实现的重试函数,它具有以下特点:
- 通用性强:可以重试任何返回 Promise 的异步函数。
- 可配置重试次数:通过参数设置最大重试次数。
- 错误处理:在所有重试都失败后,抛出最后的错误。
- 可选的延迟:可以在每次重试之间添加延迟,避免对服务器造成过大压力。
- 可选的指数退避:延迟时间可以随着重试次数的增加而指数增长,这是一种常见的优化策略,给服务器更多时间恢复。
核心概念
- Promise :JavaScript 中处理异步操作的核心。异步函数通常返回 Promise,我们可以使用
.then()
和.catch()
来处理成功和失败。 - async/await:Promise 的语法糖,让异步代码看起来更像同步代码,提高了可读性。
- 递归:在重试逻辑中,当任务失败时,函数会递归调用自身,直到成功或达到最大重试次数。
- 指数退避 (Exponential Backoff) :一种策略,在每次重试失败后,等待的时间会越来越长。这有助于避免在短时间内对不稳定的服务发起大量请求,从而加剧问题。
实现代码
js
/**
* 实现一个异步任务失败重试的方法
* @param {Function} asyncTask - 需要重试的异步函数,该函数必须返回一个 Promise。
* @param {number} maxRetries - 允许的最大重试次数。
* @param {number} [delayMs=0] - 每次重试之间的固定延迟时间(毫秒)。
* @param {boolean} [exponentialBackoff=false] - 是否启用指数退避。如果为 true,delayMs 将作为基础延迟。
* @returns {Promise<any>} - 返回异步任务的结果,如果所有重试都失败,则抛出最后一个错误。
*/
async function retryAsyncTask(
asyncTask,
maxRetries,
delayMs = 0,
exponentialBackoff = false
) {
let attempts = 0;
let lastError = null;
while (attempts <= maxRetries) {
try {
// 如果不是第一次尝试,并且设置了延迟,则等待
if (attempts > 0 && delayMs > 0) {
let currentDelay = delayMs;
if (exponentialBackoff) {
// 指数退避:延迟时间随着尝试次数指数增长
// 例如:delayMs * (2^0), delayMs * (2^1), delayMs * (2^2), ...
currentDelay = delayMs * Math.pow(2, attempts - 1);
}
console.log(`尝试 #${attempts + 1}: 等待 ${currentDelay}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, currentDelay));
}
console.log(`尝试 #${attempts + 1} / ${maxRetries + 1}: 执行任务...`);
const result = await asyncTask();
console.log(`尝试 #${attempts + 1}: 任务成功!`);
return result; // 任务成功,返回结果
} catch (error) {
lastError = error;
console.error(`尝试 #${attempts + 1}: 任务失败,错误信息:`, error);
if (attempts < maxRetries) {
attempts++;
} else {
// 达到最大重试次数,抛出最后一个错误
console.error(`所有 ${maxRetries + 1} 次尝试均失败。`);
throw lastError;
}
}
}
}
// --- 使用示例 ---
// 模拟一个可能会失败的异步任务
let callCount = 0;
function mockApiCall() {
return new Promise((resolve, reject) => {
callCount++;
console.log(`模拟 API 调用,当前是第 ${callCount} 次。`);
if (callCount < 3) { // 模拟前两次失败
reject(new Error("网络错误或服务器无响应"));
} else {
resolve("数据已成功获取!");
}
});
}
// 示例 1: 简单重试,无延迟
console.log("\n--- 示例 1: 简单重试,无延迟 ---");
callCount = 0; // 重置计数器
retryAsyncTask(mockApiCall, 2) // 允许重试 2 次,总共 3 次尝试
.then(data => {
console.log("最终成功结果:", data);
})
.catch(error => {
console.error("最终失败:", error.message);
});
// 示例 2: 重试,每次延迟 1 秒
console.log("\n--- 示例 2: 重试,每次延迟 1 秒 ---");
callCount = 0; // 重置计数器
retryAsyncTask(mockApiCall, 3, 1000) // 允许重试 3 次,总共 4 次尝试,每次延迟 1 秒
.then(data => {
console.log("最终成功结果:", data);
})
.catch(error => {
console.error("最终失败:", error.message);
});
// 示例 3: 重试,启用指数退避
console.log("\n--- 示例 3: 重试,启用指数退避 ---");
callCount = 0; // 重置计数器
// 模拟一个更难成功的任务,需要更多次尝试
function mockFlakyApiCall() {
return new Promise((resolve, reject) => {
callCount++;
console.log(`模拟 Flaky API 调用,当前是第 ${callCount} 次。`);
if (callCount < 5) { // 模拟前 4 次失败
reject(new Error("服务器暂时不可用"));
} else {
resolve("Flaky API 数据已成功获取!");
}
});
}
retryAsyncTask(mockFlakyApiCall, 4, 200, true) // 允许重试 4 次,总共 5 次尝试,基础延迟 200ms,启用指数退避
.then(data => {
console.log("最终成功结果:", data);
})
.catch(error => {
console.error("最终失败:", error.message);
});
// 示例 4: 任务始终失败的情况
console.log("\n--- 示例 4: 任务始终失败的情况 ---");
callCount = 0; // 重置计数器
function mockAlwaysFailingApiCall() {
return new Promise((resolve, reject) => {
callCount++;
console.log(`模拟 Always Failing API 调用,当前是第 ${callCount} 次。`);
reject(new Error("致命错误,无法恢复"));
});
}
retryAsyncTask(mockAlwaysFailingApiCall, 2, 500) // 允许重试 2 次,总共 3 次尝试,每次延迟 500ms
.then(data => {
console.log("最终成功结果:", data); // 不会执行
})
.catch(error => {
console.error("最终失败 (始终失败的任务):", error.message);
});
代码解释
-
retryAsyncTask(asyncTask, maxRetries, delayMs = 0, exponentialBackoff = false)
函数:asyncTask
: 这是一个函数,它应该返回一个Promise
。这是我们想要重试的异步操作。maxRetries
: 一个整数,表示在任务最终失败之前允许的最大重试次数。如果maxRetries
为0
,则任务只会被尝试一次(没有重试)。delayMs
(可选,默认为0
):每次重试之间等待的毫秒数。exponentialBackoff
(可选,默认为false
):一个布尔值,如果为true
,则delayMs
会在每次重试时指数增长。
-
attempts
和lastError
变量:attempts
: 记录当前的尝试次数(从 0 开始)。lastError
: 存储最后一次失败的错误对象,以便在所有重试都失败后抛出。
-
while (attempts <= maxRetries)
循环:- 循环会一直执行,直到任务成功或者尝试次数超过
maxRetries
。
- 循环会一直执行,直到任务成功或者尝试次数超过
-
try...catch
块:- 在
try
块中,我们await asyncTask()
来执行异步操作。 - 如果
asyncTask
成功解决 (resolved),则result
会被返回,函数结束。 - 如果
asyncTask
拒绝 (rejected),则会进入catch
块。
- 在
-
延迟逻辑:
-
if (attempts > 0 && delayMs > 0)
:只有在不是第一次尝试且设置了延迟时才等待。 -
currentDelay
计算:-
如果
exponentialBackoff
为true
,延迟时间会根据delayMs * Math.pow(2, attempts - 1)
计算。例如,如果delayMs
是 100ms:- 第一次重试 (attempts = 1): 100 * 2^0 = 100ms
- 第二次重试 (attempts = 2): 100 * 2^1 = 200ms
- 第三次重试 (attempts = 3): 100 * 2^2 = 400ms
-
否则,延迟时间固定为
delayMs
。
-
-
await new Promise(resolve => setTimeout(resolve, currentDelay))
:创建一个 Promise 并在currentDelay
毫秒后解决,从而实现等待。
-
-
重试或抛出错误:
- 在
catch
块中,如果attempts < maxRetries
,表示还有重试机会,attempts
会递增,循环会继续下一次尝试。 - 如果
attempts
已经达到maxRetries
,则表示所有重试都已用尽,此时会throw lastError
,将最后一个错误抛出。
- 在
这个实现提供了一个健壮且灵活的异步任务重试机制,适用于各种前端场景,例如 API 请求、资源加载等。