实现带重试功能的 Promise
问题描述
请完成 retryRequest
函数的实现。该函数接收一个异步任务函数 requestFn
和一个配置对象 options
,并返回一个 Promise
。retryRequest
会尝试执行 requestFn
,如果 requestFn
返回的 Promise
被拒绝(rejected),它会根据配置进行重试。
函数签名:
function retryRequest(requestFn, options = {})
参数说明
requestFn
: 一个返回 Promise 的函数。这是需要被执行并可能需要重试的异步任务。options
(可选): 一个配置对象,包含以下属性:maxRetries
(number): 最大重试次数。默认为3
。这意味着除了首次尝试之外,最多还会再尝试 3 次。retryDelay
(number): 每次重试之间的延迟时间(单位:毫秒)。默认为1000
。onRetry
(function): 一个回调函数,在每次重试尝试之前被调用。它会接收两个参数:error
(导致失败的错误) 和attempt
(当前的重试次数,从 1 开始)。
返回值
该函数返回一个 Promise
:
- 成功时:如果
requestFn
在某次尝试中成功resolve
,则retryRequest
返回的Promise
也应resolve
,并传递requestFn
的解析值。 - 失败时:如果所有尝试(包括首次尝试和所有重试)都失败了,则
retryRequest
返回的Promise
应reject
,并传递最后一次尝试所产生的错误。
示例
// 一个模拟 API 请求,它将在第二次重试时成功
let attemptCount = 0;
const flakyRequest = () => {
return new Promise((resolve, reject) => {
attemptCount++;
console.log(`发起第 ${attemptCount} 次请求...`);
if (attemptCount < 3) {
reject(new Error(`请求失败`));
} else {
resolve('成功获取数据!');
}
});
};
retryRequest(flakyRequest, {
maxRetries: 5,
retryDelay: 500,
onRetry: (error, attempt) => {
console.warn(`捕获到错误: ${error.message}。这是第 ${attempt} 次重试。`);
},
})
.then((result) => {
console.log(`最终结果: ${result}`);
})
.catch((error) => {
console.error(`所有重试均失败: ${error.message}`);
});
// ---- 预期输出 ----
// 发起第 1 次请求...
// 捕获到错误: 请求失败。这是第 1 次重试。
// (等待 500ms)
// 发起第 2 次请求...
// 捕获到错误: 请求失败。这是第 2 次重试。
// (等待 500ms)
// 发起第 3 次请求...
// 最终结果: 成功获取数据!
解题思路
解决这个问题的最佳方法是使用 async/await
和 for
循环,这能让异步的重试逻辑变得像同步代码一样清晰易读。
将函数声明为
async
: 为了在函数内部直接使用await
,我们需要将retryRequest
声明为一个async function
。这样,函数将隐式地返回一个Promise
。循环处理所有尝试: 使用一个
for
循环来控制总的尝试次数。总次数应该是1
(首次尝试) +maxRetries
(最大重试次数)。循环可以从0
到maxRetries
。使用
try...catch
: 在循环内部,使用try...catch
块来包裹对requestFn
的调用。try
块: 在这里await requestFn()
。如果这个Promise
成功resolve
,说明任务成功了,我们直接return
结果。这个return
会resolve
整个async
函数返回的Promise
,流程结束。catch
块: 如果requestFn()
失败并抛出错误,catch
块会被执行。在这里我们处理重试逻辑。
处理重试逻辑:
- 检查是否为最后一次尝试: 在
catch
块中,首先检查当前是否是最后一次允许的尝试(即attempt === maxRetries
)。如果是,说明所有重试机会都已用尽,我们应该通过throw error
来reject
整个async
函数的Promise
。 - 执行
onRetry
回调: 如果还有重试机会,检查并调用onRetry
回调函数,向其传递当前的错误和尝试次数。 - 实现延迟: 使用
new Promise(resolve => setTimeout(resolve, retryDelay))
创建一个延时Promise
,并await
它。这样可以暂停执行,等待指定的retryDelay
时间后再进入下一次循环。
- 检查是否为最后一次尝试: 在
通过这个结构,代码可以清晰地表达“尝试 -> 失败 -> 等待 -> 再次尝试”的循环,直到成功或次数耗尽为止。
代码实现
/**
* 实现一个基于Promise的请求重试机制。
* @param {() => Promise<any>} requestFn - 一个返回 Promise 的函数,代表需要执行的异步任务。
* @param {object} [options={}] - 可选的配置对象。
* @param {number} [options.maxRetries=3] - 最大重试次数。
* @param {number} [options.retryDelay=1000] - 每次重试之间的延迟(毫秒)。
* @param {(error: any, attempt: number) => void} [options.onRetry=null] - 每次重试前调用的回调函数。
* @returns {Promise<any>}
*/
async function retryRequest(requestFn, options = {}) {
const { maxRetries = 3, retryDelay = 1000, onRetry = null } = options;
// 循环处理所有尝试,从首次尝试(i=0)到最后一次重试(i=maxRetries)
for (let i = 0; i <= maxRetries; i++) {
try {
// 尝试执行请求
const result = await requestFn();
// 如果成功,立即返回结果,Promise 将会 resolve
return result;
} catch (error) {
// 如果这是最后一次尝试,则抛出错误,Promise 将会 reject
if (i === maxRetries) {
throw error;
}
// 如果 onRetry 回调存在,则调用它
if (onRetry && typeof onRetry === 'function') {
// 传递的 attempt 是从 1 开始的
onRetry(error, i + 1);
}
// 等待指定的延迟时间
if (retryDelay > 0) {
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
}
}
// --- 测试用例 ---
// 1. 测试在第二次重试时成功
let attemptCount1 = 0;
const requestThatSucceeds = () =>
new Promise((resolve, reject) => {
attemptCount1++;
console.log(`[测试1] 发起第 ${attemptCount1} 次请求...`);
if (attemptCount1 < 3) {
reject(new Error(`请求失败`));
} else {
resolve('成功获取数据!');
}
});
console.log('--- 开始测试用例 1: 重试后成功 ---');
retryRequest(requestThatSucceeds, {
maxRetries: 5,
retryDelay: 200,
onRetry: (error, attempt) => {
console.warn(
`[测试1] 捕获到错误: ${error.message}。第 ${attempt} 次重试...`
);
},
})
.then((result) => console.log(`[测试1] 最终结果: ${result}\n`))
.catch((error) => console.error(`[测试1] 最终失败: ${error.message}\n`));
// 2. 测试所有重试都失败
let attemptCount2 = 0;
const requestThatFails = () =>
new Promise((resolve, reject) => {
attemptCount2++;
console.log(`[测试2] 发起第 ${attemptCount2} 次请求...`);
reject(new Error(`永久性错误`));
});
// 使用.then()来等待上一个测试完成
new Promise((r) => setTimeout(r, 1000)).then(() => {
console.log('--- 开始测试用例 2: 所有重试均失败 ---');
retryRequest(requestThatFails, {
maxRetries: 2,
retryDelay: 200,
})
.then((result) => console.log(`[测试2] 最终结果: ${result}\n`))
.catch((error) => console.error(`[测试2] 最终失败: ${error.message}\n`));
});