本系列上一篇文章:【代码鉴赏】简略优雅的 JavaScript 代码片段(一):异步控制
流控(又称限流,管制调用频率)
后端为了保证系统稳固运行,往往会对调用频率进行限度(比方每人每秒不得超过 10 次)。为了防止造成资源节约或者蒙受零碎惩办,前端也须要被动限度本人调用 API 的频率。
前端须要大批量拉取列表时,或者须要对每一个列表项调用 API 查问详情时,尤其须要进行限流。
这里提供一个流控工具函数wrapFlowControl
,它的益处是:
- 应用简略、对调用者通明:只须要包装一下你本来的异步函数,即可失去领有的流控限度的函数,它与本来的异步函数应用形式雷同。实用于任何异步函数。
const apiWithFlowControl = wrapFlowControl(callAPI, 2);
- 不会疏忽任何一次调用(不像防抖或节流)。每一次调用都会被执行、失去相应的后果。只不过可能会为了管制频率而被提早执行。
应用示例:
// 创立了一个调度队列
const apiWithFlowControl = wrapFlowControl(callAPI, 2);
// ......
<button
onClick={() => {
const count = ++countRef.current;
// 申请调度队列安顿一次函数调用
apiWithFlowControl(count).then((result) => {// do something with api result});
}}
>
Call apiWithFlowControl
</button>
codesandbox 在线示例
这个计划的实质是,先通过 wrapFlowControl
创立了一个调度队列,而后在每次调用 apiWithFlowControl
的时候,申请调度队列安顿一次函数调用。
wrapFlowControl
的代码实现:
const ONE_SECOND_MS = 1000;
/**
* 管制函数调用频率。在任何一个 1 秒的区间,调用 fn 的次数不会超过 maxExecPerSec 次。* 如果函数触发频率超过限度,则会延缓一部分调用,使得理论调用频率满足下面的要求。*/
export function wrapFlowControl<Args extends any[], Ret>(fn: (...args: Args) => Promise<Ret>,
maxExecPerSec: number
) {if (maxExecPerSec < 1) throw new Error(`invalid maxExecPerSec`);
const queue: QueueItem[] = [];
const executed: ExecutedItem[] = [];
return function wrapped(...args: Args): Promise<Ret> {return enqueue(args);
};
function enqueue(args: Args): Promise<Ret> {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);
// ......
<div style={{marginTop: 24}}>
<button
onClick={() => {
const count = ++countRef.current;
// 在调用时才决定要执行的异步操作
// 将异步操作退出调度队列
scheduleCallWithFlowControl(async () => {
// 流控会保障这个异步函数的执行频率
if (count % 2 === 1) {return callAPI1(count);
} else {return callAPI2(count);
}
}).then((result) => {// do something with api result});
}}
>
Call scheduleCallWithFlowControl
</button>
</div>
codesandbox 在线示例
这个计划的实质是,先通过 createFlowControlScheduler
创立了一个调度队列,而后每当 scheduleCallWithFlowControl
承受到一个异步工作,就会将它退出调度队列。调度队列会确保所有异步工作都被调用(依照退出队列的程序),并且工作执行频率不超过指定的值。
createFlowControlScheduler
的实现其实非常简单,基于后面的 wrapFlowControl
实现:
/**
* 相似于 wrapFlowControl,只不过将 task 的定义提早到调用 wrapper 时才提供,* 而不是在创立 flowControl wrapper 时就提供
*/
export function createFlowControlScheduler(maxExecPerSec: number) {return wrapFlowControl(async <T>(task: () => Promise<T>) => {return task();
}, maxExecPerSec);
}