一、不那么优雅的逻辑
「重试」是前端开发中一个非常常见的场景,但因为 Promise
的异步回调特性,导致我们在处理重试逻辑的时候很容易就陷入到了「回调地狱」当中,比如这种递归和 Promise
结合写的重试函数:
// 工具函数,用于延迟一段时间
const sleep = (second: number = 1000) => {
return new Promise((resolve) => {
setTimeout(resolve, second);
});
};
// 工具函数,用于递归重试函数
const retry = <T>(
promise: () => Promise<T>,
resolve: (value: unknown) => void,
reject: (reason?: any) => void,
retryTimes: number,
retryMaxTimes: number
) => {
sleep().then(() => {
promise()
.then((res: any) => {
resolve(res);
})
.catch((err: any) => {
if (retryTimes >= retryMaxTimes) {
reject(err);
return;
}
retry(promise, resolve, reject, retryTimes + 1, retryMaxTimes);
});
});
};
// 重试函数
export const retryRequest = <T>(
promise: () => Promise<T>,
retryMaxTimes: number
) => {
return new Promise((resolve, reject) => {
let count = 1;
promise()
.then((res) => {
resolve(res);
})
.catch((err) => {
if (count >= retryMaxTimes) {
reject(err);
return;
}
retry(promise, resolve, reject, count + 1, retryMaxTimes);
});
});
};
而使用 async
和 await
的话,情况也好不了太多,我们还是要用递归,而且还要在 await
函数外面包一层「try - catch」也不太直观:
// 工具函数,用于延迟一段时间
const sleep = (second: number = 1000) => {
return new Promise((resolve) => {
setTimeout(resolve, second);
});
};
// 工具函数,用于递归重试函数
const retry = async <T>(
promise: () => Promise<T>,
retryTimes: number,
retryMaxTimes: number
) => {
await sleep();
try {
let result = await promise();
return result;
} catch (err) {
if (retryTimes >= retryMaxTimes) {
throw err;
}
retry(promise(), retryTimes + 1, retryMaxTimes);
}
};
// 重试函数
export const retryRequest = async <T>(
promise: () => Promise<T>,
retryMaxTimes: number
) => {
let count = 1;
try {
let result = await promise();
return result;
} catch (err) {
if (count >= retryMaxTimes) {
throw err;
}
retry(promise(), count + 1, retryMaxTimes);
}
};
二、优雅的逻辑
那么怎么样优雅的写重试逻辑呢?结合第一部分中那些不那么优雅的逻辑,我们可以知道,如果想要提升可读性,我们得做如下三件事:
1、消灭回调 2、消灭 try - catch 这种异常处理逻辑 3、消灭递归逻辑
2-1、回调以及异常处理
为了消灭回调函数,我们还是选择了 async
和 await
语法,而对于异常处理,我们则封装了一个函数,把异常处理逻辑拉平,就像下面这样:
const awaitErrorWrap = async <T, U = any>(
promise: Promise<T>
): Promise<[U | null, T | null]> => {
try {
const data = await promise;
return [null, data];
} catch (err: any) {
return [err, null];
}
};
这样我们就可以像 go
语言一样,不用包裹 try - catch,直接拿到异常了,就像下面这样:
const [err, data] = await awaitErrorWrap(promise);
2-2、将递归函数改成循环
我们将原有的递归函数改成一个 for
循环,这样逻辑就更清晰了:
const retryRequest = async <T>(
promise: () => Promise<T>,
retryTimes: number = 3
) => {
let output: [any, T | null] = [null, null];
for (let a = 0; a < retryTimes; a++) {
output = await awaitErrorWrap(promise());
if (output[1]) {
break;
}
}
return output;
};
2-3、优雅的重试逻辑
那么我们将上面的代码组合起来,就得出了下面这个很简洁明了且优雅的重试逻辑了:
// 工具函数,用于延迟一段时间
const sleep = (time: number) => {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
};
// 工具函数,用于包裹 try - catch 逻辑
const awaitErrorWrap = async <T, U = any>(
promise: Promise<T>
): Promise<[U | null, T | null]> => {
try {
const data = await promise;
return [null, data];
} catch (err: any) {
return [err, null];
}
};
// 重试函数
export const retryRequest = async <T>(
promise: () => Promise<T>,
retryTimes: number = 3,
retryInterval: number = 500
) => {
let output: [any, T | null] = [null, null];
for (let a = 0; a < retryTimes; a++) {
output = await awaitErrorWrap(promise());
if (output[1]) {
break;
}
console.log(`retry ${a + 1} times, error: ${output[0]}`);
await sleep(retryInterval);
}
return output;
};
我们可以这样使用它:
import { retryRequest } from "xxxx";
import axios from "axios";
const request = (url: string) => {
return axios.get(url);
};
const [err, data] = await retryRequest(request("https://request_url"), 3, 500);
2-4、更进一步?写个装饰器吧
当然写到这里肯定有朋友会说:“每次在 Promise
外面包一层这个函数也不够优雅!”
没问题,我们可以更进一步,写一个 typescript
的装饰器:
export const retryDecorator = (
retryTimes: number = 3,
retryInterval: number = 500
): MethodDecorator => {
return (_: any, __: string | symbol, descriptor: any) => {
const fn = descriptor.value;
descriptor.value = async function (...args: any[]) {
// 这里的 retryRequest 就是刚才的重试函数
return retryRequest(fn.apply(this, args), retryTimes, retryInterval);
};
};
};
这样我们就可以直接这样使用这个装饰器了:
import { retryRequest } from "xxxx";
import axios from "axios";
class RequestClass {
@retryDecorator(3, 500)
static async getUrl(url: string) {
return axios.get(url);
}
}
const [err, data] = await RequestClass.getUrl("https://request_url");
2-5、再优化一下?
评论区有朋友认为这样把异常直接作为变量抛出来不符合直觉,没问题,我们换一个能正常用 try catch 的版本:
// 重试函数
export const retryRequest = async <T>(
promise: () => Promise<T>,
retryTimes: number = 3,
retryInterval: number = 500
) => {
let output: [any, T | null] = [null, null];
for (let a = 0; a < retryTimes; a++) {
output = await awaitErrorWrap(promise());
if (output[1]) {
break;
}
console.log(`retry ${a + 1} times, error: ${output[0]}`);
await sleep(retryInterval);
}
if (output[0]) {
throw output[0];
}
return output[1];
};
我们可以正常按照 try catch 的方式使用它:
import { retryRequest } from "xxxx";
import axios from "axios";
const request = (url: string) => {
return axios.get(url);
};
try {
const res = await retryRequest(request("https://request_url"), 3, 500);
} catch (err) {
console.error(res);
}