【代码赏析】简洁优雅的JavaScript代码片段(二):流量控制与重试限流,控制调用频率)后端为了保证系统的稳定运行,往往会限制调用频率(比如不超过每人每秒10次)。为了避免浪费资源或者被系统惩罚,前端还需要主动限制调用API的频率。当前端需要大量拉取列表,或者需要调用API查询每个列表项的详情时,限流就显得尤为必要。这里有一个流量控制工具函数wrapFlowControl。它的优点是:使用方便,对调用者透明:你只需要包装你原来的异步函数就可以得到一个有流控限制的函数。它的使用方式与原来的异步函数相同。相同的。constapiWithFlowControl=wrapFlowControl(callAPI,2);不会丢弃任何呼叫(与去抖动或节流不同)。每一次调用都会被执行,并得到相应的结果。只是为了控制频率可能会延迟。用法示例://创建调度队列constapiWithFlowControl=wrapFlowControl(callAPI,2);//......{constcount=++countRef.current;//request调度队列安排一个函数调用apiWithFlowControl(count).then((result)=>{//dosomethingwithapiresult});}}>调用apiWithFlowControlcodesandbox在线例子这个方案的本质是先通过wrapFlowControl创建一个dispatchqueue,然后在每次调用apiWithFlowControl时请求dispatchqueue调度一个函数调用。代码实现wrapFlowControl的代码实现:constONE_SECOND_MS=1000;/***控制函数调用的频率。在任意1秒的时间间隔内,调用fn的次数不会超过maxExecPerSec次。*如果函数触发频率超过限制,将延迟部分调用,使实际调用频率满足上述要求。*/导出函数wrapFlowControl(fn:(...args:Args)=>Promise,maxExecPerSec:number){if(maxExecPerSec<1)thrownewError(`invalidmaxExecPerSec`);//调度队列,记录要执行的任务constqueue:QueueItem[]=[];//上一秒的执行记录,用于判断执行频率是否超过限制constexecuted:ExecutedItem[]=[];返回函数wrapped(...args:Args):Promise{returnenqueue(args);};functionenqueue(args:Args):Promise{returnnewPromise((resolve,reject)=>{queue.push({args,resolve,reject});scheduleCheckQueue();});}functionscheduleCheckQueue(){constnextTask=queue[0];//仅当队列为空时停止scheduleCheckQueue递归调用if(!nextTask)return;清洁执行();if(executed.length{//此时可以清除earliestExecuted,交给下一个任务},waitTime);}}functioncleanExecuted(){constnow=newDate().valueOf();constoneSecondAgo=now-ONE_SECOND_MS;while(executed[0]?.timestamp<=oneSecondAgo){执行。转移();}}functionexecute({args,resolve,reject}:QueueItem){consttimestamp=newDate().的价值();fn(...参数)。然后(解决,拒绝);executed.push({时间戳});}类型QueueItem={args:Args;解决:(ret:Ret)=>无效;拒绝:(错误:任何)=>无效;};输入ExecutedItem={时间戳:数字;};}延时判断函数逻辑从上面的例子可以看出,在使用wrapFlowControl时,需要预先定义异步函数callAPI的逻辑,才能得到流控函数。但是在一些特殊的场景下,我们需要在发起调用的时候,确定异步函数应该执行什么样的逻辑。也就是说,“在定义时确定”推迟到“在调用时确定”。所以我们实现了另一个实用函数createFlowControlScheduler。在上面的使用示例中,DemoWrapFlowControl就是一个例子:我们决定在用户??点击按钮时是调用API1还是调用API2。//创建调度队列constscheduleCallWithFlowControl=createFlowControlScheduler(2);//......{constcount=++countRef.current;//只有在调用异步操作时才决定执行//将异步操作加入调度队列//这两个异步操作共享一个流控配额if(count%2===1){scheduleCallWithFlowControl(()=>callAPI1(count)).then((result)=>{//使用api1result});}else{scheduleCallWithFlowControl(()=>callAPI2(count)).then((result)=>{//用api2结果做点什么});}}}>调用scheduleCallWithFlowControlcodesandbox在线例子这个方案的本质是先通过createFlowControlScheduler创建一个调度队列,然后每当scheduleCallWithFlowControl接收到一个异步任务,就会加入到调度队列中。调度队列确保所有异步任务都被调用(按照它们入队的顺序)并且任务执行频率不超过指定值。createFlowControlScheduler的实现其实很简单,基于前面的wrapFlowControl实现:/***和wrapFlowControl类似,只是任务的定义是延迟到wrapper被调用的时候,*而不是在创建的时候提供*/export函数createFlowControlSchedulertheflowControlwrapper(maxExecPerSec:number){returnwrapFlowControl(async(task:()=>Promise)=>{returntask();},maxExecPerSec);}关于如何转换我们的效用的扩展思考功能使其能够支持“每分钟不超过n次”的频率限制吗?或者让它支持“正在进行的任务数不能超过n”的限制?如何改造我们的工具函数,使其同时支持“每秒不超过n次”和“每分钟不超过m次”的频率限制?如何实现更灵活的调度队列,让不同的调度约束可以结合起来?例如,频率限制为“每秒不超过10次”和“每分钟不超过30次”。它的意义在于允许在短时间内突然出现高频调用(通过放宽秒级限制),同时防止高频调用持续时间过长(通过分钟级限制)。重试我们已经有了一个解决方案来限制前端的调用频率。但是,即使我们对前端的调用频率进行了限制,还是有可能会遇到错误:前端的流控不能完全满足后端的流控限制。后端可能会对来自所有用户的调用总和施加总体限制。比如所有用户的调用频率不能超过10000次/s,前端流控不能对齐这个限制。非流体错误。例如,后台服务或网络不稳定,导致暂时不可用。所以,面对前端这些不可避免的错误,需要通过重试来得到结果。这里有一个重试工具函数wrapRetry。它的优点是:使用方便,对调用者透明:就像之前的流控工具功能一样,你只需要将你原来的异步函数包装起来,得到一个自动重试的功能。它的使用方式与原来的异步函数相同。支持自定义重试错误类型、重试次数、重试等待时间。用法:constapiWithRetry=wrapRetry(callAPI,(error,retryCount)=>error.type==="throttle"&&retryCount<=5);它的用法类似于wrapFlowControl。代码实现wrapRetry代码实现:/***捕捉到特定失败后会重试。适用于无副作用的手术。*比如数据请求可能会被流控拦截,所以可以用来自动重试。*/导出函数wrapRetry(fn:(...args:Args)=>Promise,shouldRetry:(error:any,retryCount:number)=>boolean,startRetryWait:number=1000){returnasyncfunctionwrapped(...args:Args):Promise{returncallFn(args,startRetryWait,0);};异步函数callFn(args:Args,wait:number,retryCount:number):Promise{try{returnawaitfn(...args);}catch(error){if(shouldRetry(error,retryCount)){if(wait>0)awaittimeout(wait);//nextWait是wait的1~2倍//如果startRetryWait为0,则wait始终为0constnextWait=wait*(Math.random()+1);返回callFn(args,nextWait,retryCount+1);}else{抛出错误;}}}}functiontimeout(wait:number){returnnewPromise((res)=>{setTimeout(()=>{res(null);},wait);});}其中,我们添加了一个优化要点:让重试等待时间逐渐增加。例如,第二次重试的等待时间是第一次重试等待时间的1到2倍。这是为了尽量减少调用次数,避免给不稳定的后端带来更多压力。没有选择增加一倍,是为了避免等待重试的时间过长,降低用户体验。可组合性值得一提的是,自动重试可以与之前的流量限制工具结合使用(由于它们对调用者是透明的,并且不会改变函数的使用方式):constapiWithFlowControl=wrapFlowControl(callAPI,2);constapiWithRetry=wrapRetry(apiWithFlowControl,(error,retryCount)=>error.type==="throttle"&&retryCount<=5);注意,throttling包在里面,retry包在外面,保证retry发起的请求也能被throttle。