【代码鉴赏】简单优雅的JavaScript代码片段(二)(流控和重试)
本系列上一篇文章: 【代码鉴赏】简单优雅的JavaScript代码片段(一):异步控制流控(又称限流,控制调用频率) 后端为了保证系统稳定运行,往往会对调用频率进行限制(比如每人每秒不得超过10次)。为了避免造成资源浪费或者遭受系统惩罚,前端也需要主动限制自己调用API的频率。
前端需要大批量拉取列表时,或者需要对每一个列表项调用API查询详情时,尤其需要进行限流。这里提供一个流控工具函数
wrapFlowControl
,它的好处是:- 使用简单、对调用者透明:只需要包装一下你原本的异步函数,即可得到拥有的流控限制的函数,它与原本的异步函数使用方式相同。适用于任何异步函数。
const apiWithFlowControl = wrapFlowControl(callAPI, 2);
- 不会忽略任何一次调用(不像防抖或节流)。每一次调用都会被执行、得到相应的结果。只不过可能会为了控制频率而被延迟执行。
// 创建了一个调度队列
const apiWithFlowControl = wrapFlowControl(callAPI, 2);
// ......
codesandbox在线示例
这个方案的本质是,先通过
wrapFlowControl
创建了一个调度队列,然后在每次调用apiWithFlowControl
的时候,请求调度队列安排一次函数调用。wrapFlowControl
的代码实现:const ONE_SECOND_MS = 1000;
/**
* 控制函数调用频率。在任何一个1秒的区间,调用fn的次数不会超过maxExecPerSec次。
* 如果函数触发频率超过限制,则会延缓一部分调用,使得实际调用频率满足上面的要求。
*/
export function wrapFlowControl(
fn: (...args: Args) => Promise,
maxExecPerSec: number
) {
if (maxExecPerSec < 1) throw new Error(`invalid maxExecPerSec`);
const queue: QueueItem[] = [];
const executed: ExecutedItem[] = [];
return function wrapped(...args: Args): Promise {
return enqueue(args);
};
function enqueue(args: Args): Promise {
return new Promise((resolve, reject) => {
queue.push({ args, resolve, reject });
scheduleCheckQueue();
});
}function scheduleCheckQueue() {
const nextTask = queue[0];
// 仅在queue为空时,才会停止scheduleCheckQueue递归调用
if (!nextTask) return;
cleanExecuted();
if (executed.length < maxExecPerSec) {
// 最近一秒钟执行的数量少于阈值,才可以执行下一个task
queue.shift();
execute(nextTask);
scheduleCheckQueue();
} else {
// 过一会再调度
const earliestExecuted = executed[0];
const now = new Date().valueOf();
const waitTime = earliestExecuted.timestamp + ONE_SECOND_MS - now;
setTimeout(() => {
// 此时earliestExecuted已经可以被清除,给下一个task的执行提供配额
scheduleCheckQueue();
}, waitTime);
}
}function cleanExecuted() {
const now = new Date().valueOf();
const oneSecondAgo = now - ONE_SECOND_MS;
while (executed[0]?.timestamp <= oneSecondAgo) {
executed.shift();
}
}function execute({ args, resolve, reject }: QueueItem) {
const timestamp = new Date().valueOf();
fn(...args).then(resolve, reject);
executed.push({ timestamp });
}type QueueItem = {
args: Args;
resolve: (ret: Ret) => void;
reject: (error: any) => void;
};
type ExecutedItem = {
timestamp: number;
};
}
延迟确定函数逻辑
从上面的示例可以看出,在使用
wrapFlowControl
的时候,你需要预先定义好异步函数callAPI
的逻辑,才能得到流控函数。但是在一些特殊场景中,我们需要在发起调用的时候,再确定异步函数应该执行什么逻辑。即将“定义时确定”推迟到“调用时确定”。因此我们实现了另一个工具函数
createFlowControlScheduler
。在上面的使用示例中,
DemoWrapFlowControl
就是一个例子:我们在用户点击按钮的时候,才决定要调用API1还是API2。// 创建一个调度队列
const scheduleCallWithFlowControl = createFlowControlScheduler(2);
// ......
codesandbox在线示例
这个方案的本质是,先通过
createFlowControlScheduler
创建了一个调度队列,然后每当scheduleCallWithFlowControl
接受到一个异步任务,就会将它加入调度队列。调度队列会确保所有异步任务都被调用(按照加入队列的顺序),并且任务执行频率不超过指定的值。【【代码鉴赏】简单优雅的JavaScript代码片段(二)(流控和重试)】
createFlowControlScheduler
的实现其实非常简单,基于前面的wrapFlowControl
实现:/**
* 类似于wrapFlowControl,只不过将task的定义延迟到调用wrapper时才提供,
* 而不是在创建flowControl wrapper时就提供
*/
export function createFlowControlScheduler(maxExecPerSec: number) {
return wrapFlowControl(async (task: () => Promise) => {
return task();
}, maxExecPerSec);
}
推荐阅读
- 宽容谁
- 我要做大厨
- CVE-2020-16898|CVE-2020-16898 TCP/IP远程代码执行漏洞
- 增长黑客的海盗法则
- 画画吗()
- 2019-02-13——今天谈梦想()
- 远去的风筝
- 三十年后的广场舞大爷
- 叙述作文
- 20190302|20190302 复盘翻盘