关于javascript:代码鉴赏简单优雅的JavaScript代码片段二流控和重试

本系列上一篇文章:【代码鉴赏】简略优雅的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);
}

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理