一个异步任务失败重试的方法

在前端实现一个异步任务失败重试的方法,同时接收允许重试的次数,这是一个非常常见的需求,特别是在处理网络请求时。

以下是一个使用 async/awaitPromise 实现的重试函数,它具有以下特点:

  • 通用性强:可以重试任何返回 Promise 的异步函数。
  • 可配置重试次数:通过参数设置最大重试次数。
  • 错误处理:在所有重试都失败后,抛出最后的错误。
  • 可选的延迟:可以在每次重试之间添加延迟,避免对服务器造成过大压力。
  • 可选的指数退避:延迟时间可以随着重试次数的增加而指数增长,这是一种常见的优化策略,给服务器更多时间恢复。

核心概念

  1. Promise :JavaScript 中处理异步操作的核心。异步函数通常返回 Promise,我们可以使用 .then().catch() 来处理成功和失败。
  2. async/await:Promise 的语法糖,让异步代码看起来更像同步代码,提高了可读性。
  3. 递归:在重试逻辑中,当任务失败时,函数会递归调用自身,直到成功或达到最大重试次数。
  4. 指数退避 (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);
  });

代码解释

  1. retryAsyncTask(asyncTask, maxRetries, delayMs = 0, exponentialBackoff = false) 函数

    • asyncTask: 这是一个函数,它应该返回一个 Promise。这是我们想要重试的异步操作。
    • maxRetries: 一个整数,表示在任务最终失败之前允许的最大重试次数。如果 maxRetries0,则任务只会被尝试一次(没有重试)。
    • delayMs (可选,默认为 0):每次重试之间等待的毫秒数。
    • exponentialBackoff (可选,默认为 false):一个布尔值,如果为 true,则 delayMs 会在每次重试时指数增长。
  2. attemptslastError 变量

    • attempts: 记录当前的尝试次数(从 0 开始)。
    • lastError: 存储最后一次失败的错误对象,以便在所有重试都失败后抛出。
  3. while (attempts <= maxRetries) 循环

    • 循环会一直执行,直到任务成功或者尝试次数超过 maxRetries
  4. try...catch

    • try 块中,我们 await asyncTask() 来执行异步操作。
    • 如果 asyncTask 成功解决 (resolved),则 result 会被返回,函数结束。
    • 如果 asyncTask 拒绝 (rejected),则会进入 catch 块。
  5. 延迟逻辑

    • if (attempts > 0 && delayMs > 0):只有在不是第一次尝试且设置了延迟时才等待。

    • currentDelay 计算:

      • 如果 exponentialBackofftrue,延迟时间会根据 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 毫秒后解决,从而实现等待。

  6. 重试或抛出错误

    • catch 块中,如果 attempts < maxRetries,表示还有重试机会,attempts 会递增,循环会继续下一次尝试。
    • 如果 attempts 已经达到 maxRetries,则表示所有重试都已用尽,此时会 throw lastError,将最后一个错误抛出。

这个实现提供了一个健壮且灵活的异步任务重试机制,适用于各种前端场景,例如 API 请求、资源加载等。

相关推荐
再学一点就睡2 小时前
手写 Promise 静态方法:从原理到实现
前端·javascript·面试
再学一点就睡3 小时前
前端必会:Promise 全解析,从原理到实战
前端·javascript·面试
前端工作日常3 小时前
我理解的eslint配置
前端·eslint
前端工作日常4 小时前
项目价值判断的核心标准
前端·程序员
90后的晨仔4 小时前
理解 Vue 的列表渲染:从传统 DOM 到响应式世界的演进
前端·vue.js
OEC小胖胖5 小时前
性能优化(一):时间分片(Time Slicing):让你的应用在高负载下“永不卡顿”的秘密
前端·javascript·性能优化·web
烛阴5 小时前
ABS - Rhomb
前端·webgl
植物系青年5 小时前
10+核心功能点!低代码平台实现不完全指南 🧭(下)
前端·低代码
植物系青年5 小时前
10+核心功能点!低代码平台实现不完全指南 🧭(上)
前端·低代码
桑晒.5 小时前
CSRF漏洞原理及利用
前端·web安全·网络安全·csrf