如何基于fetch响应异步处理动态数组实现并发请求处理
实现方案
原串行逻辑慢的核心原因是每次只发一个请求,大量时间浪费在等IO返回,完全可以做并发处理,下面给两个可直接用的实现,都满足「所有节点完整处理、不要求顺序」的需求。
版本1:无并发上限最简实现
逻辑最直白,按批次把当前待处理的所有节点一次性并发请求,等整批返回后再处理下一批发现的新节点,加了去重逻辑避免重复请求同一个节点。
// 记录已经请求过的节点,避免重复发请求 const visited = new Set(); // 待处理节点队列 let pendingNodes = [ancestor]; visited.add(ancestor); while (pendingNodes.length > 0) { // 一次性并发发起当前所有待处理节点的请求 const relativesResult = await Promise.all( pendingNodes.map(node => fetchRelatives(node)) ); // 收集这一批返回的、没处理过的祖先节点,作为下一批的待处理队列 const nextBatch = []; for (const relative of relativesResult) { for (const ancestorItem of relative.ancestors) { if (!visited.has(ancestorItem)) { visited.add(ancestorItem); nextBatch.push(ancestorItem); } } } pendingNodes = nextBatch; }
这个版本适合节点总量不大的场景,代码短不容易写错。如果单批节点量很大,一次性发几百上千个请求容易触发接口限流,建议用下面带并发控制的版本。
版本2:带并发上限的生产可用实现
支持自定义最大并发数,而且哪个请求先返回就立刻处理它的结果、塞新任务进队列,不需要等同批次其他慢请求,资源利用率更高。
// 按接口限流规则配置最大并发数,浏览器场景一般设3-10即可 const MAX_CONCURRENT = 6; const visited = new Set(); const taskQueue = [ancestor]; visited.add(ancestor); let runningTaskCount = 0; let finishResolve; // 用Promise控制全流程结束时机 const allProcessDone = new Promise(resolve => { finishResolve = resolve; }); function run() { // 队列空了、也没有正在跑的任务,说明全量节点处理完成 if (taskQueue.length === 0 && runningTaskCount === 0) { finishResolve(); return; } // 没到并发上限就一直从队列拉任务跑 while (runningTaskCount < MAX_CONCURRENT && taskQueue.length > 0) { const currentNode = taskQueue.pop(); runningTaskCount++; fetchRelatives(currentNode) .then(res => { // 请求一返回就立刻处理新发现的节点,不用等其他请求 for (const ancestorItem of res.ancestors) { if (!visited.has(ancestorItem)) { visited.add(ancestorItem); taskQueue.push(ancestorItem); } } }) .finally(() => { runningTaskCount--; // 当前任务结束,立刻拉新任务补位 run(); }); } } // 启动任务 run(); // 等待所有节点处理完成 await allProcessDone;
注意事项
- 如果你的节点是对象类型,
visited不要直接存对象引用,改成存节点的唯一标识(比如node.id),避免引用不同导致去重失效 - 不要把并发数设得过高,否则容易触发浏览器同域名连接数限制、或者服务端限流规则
- 两个版本的最终处理结果和原串行逻辑完全一致,只是请求并发度更高,执行速度会快数倍到数十倍不等,取决于树的深度和单请求耗时。
内容的提问来源于stack exchange,提问作者Antoine Vo
相关产品推荐
相关产品推荐

