getImage函数并发请求数限制(n=1场景)方案失效排查与修复
问题分析与解决方案
原函数与需求
原同步函数实现:
export function getImage(requestParameters: TRequestParameters): TRequest<TResponse<ImageBitmap | HTMLImageElement>> { const request = helper.getArrayBuffer(requestParameters); return { response: (async () => { const response = await request.response; const image = await arrayBufferToCanvasImageSource(response.data); return { data: image, cacheControl: response.cacheControl, expires: response.expires }; })(), cancel: request.cancel }; }
函数返回对象包含:
response:Promise,解析后返回带data、cacheControl、expires的结果cancel:取消请求的方法
需求:限制并发请求数为n:
n=0:禁止发起请求n=1:顺序加载(同一时间仅一个请求)1<n<m:最多同时运行n个请求
你的方案失效原因
你用ongoingImageRequests维护正在运行的请求,但逻辑存在致命问题:
当同时调用多次getImage时,后续请求的await Promise.allSettled(ongoingImageRequests.map(...))只能等待当前已经在运行的请求,但无法等待前面还在等待队列里的请求。比如第三次调用getImage时,第二个请求还没发起(因为它在等第一个请求完成),此时ongoingImageRequests里只有第一个请求,第三次请求会在第一个完成后直接发起,和第二个请求并行,破坏了顺序要求。
简单说:你只跟踪了正在运行的请求,没维护等待发起的请求队列,导致后续请求会“插队”并行执行。
实现n=1的顺序加载
我们需要维护一个链式Promise队列,让每个新请求都等待前一个请求完成后再发起,确保严格顺序:
// 维护请求链,记录上一个请求的完成状态 let requestChain: Promise<void> = Promise.resolve(); // 存储当前活跃请求,用于取消管理 const activeRequests: Array<{ cancel: () => void }> = []; export function getImage(requestParameters: TRequestParameters): TRequest<TResponse<ImageBitmap | HTMLImageElement>> { if (webpSupported.supported) { if (!requestParameters.headers) requestParameters.headers = {}; requestParameters.headers['Accept'] = 'image/webp,*/*'; } let cancelFn: (() => void) | undefined; let isCanceled = false; const responsePromise = (async () => { // 等待前面所有请求完成 await requestChain; if (isCanceled) { throw new Error('Request canceled'); } // 发起当前请求 const request = helper.getArrayBuffer(requestParameters); cancelFn = request.cancel; activeRequests.push(request); try { const response = await request.response; if (isCanceled) throw new Error('Request canceled'); const image = await arrayBufferToCanvasImageSource(response.data); return { data: image, cacheControl: response.cacheControl, expires: response.expires }; } finally { // 从活跃列表移除当前请求 const idx = activeRequests.indexOf(request); if (idx !== -1) activeRequests.splice(idx, 1); // 更新请求链:当前请求完成后,允许下一个请求执行 requestChain = requestChain.then(() => Promise.resolve()); } })(); return { response: responsePromise, cancel() { isCanceled = true; cancelFn?.(); // 捕获取消导致的Promise拒绝,避免控制台报错 responsePromise.catch(() => {}); } }; }
关键逻辑说明
requestChain:一个不断延伸的Promise链,每个新请求都会await requestChain,确保自己在所有之前的请求完成后才执行。- 取消处理:
- 用
isCanceled标记请求状态,即使请求还在队列中未发起,也能直接拒绝Promise。 - 如果请求已经发起,调用
cancelFn取消网络请求。
- 用
- finally块:无论请求成功、失败还是被取消,都会更新请求链,让后续请求可以继续执行,同时清理活跃请求列表。
扩展到任意并发数n
如果需要支持自定义并发数,比如MAX_CONCURRENT=3,可以用“活跃计数+等待队列”的方式实现:
// 配置最大并发数 const MAX_CONCURRENT = 3; // 当前活跃请求数 let activeCount = 0; // 等待队列:存储等待并发槽位的请求的resolve函数 const waitingQueue: Array<() => void> = []; export function getImage(requestParameters: TRequestParameters): TRequest<TResponse<ImageBitmap | HTMLImageElement>> { if (webpSupported.supported) { if (!requestParameters.headers) requestParameters.headers = {}; requestParameters.headers['Accept'] = 'image/webp,*/*'; } let cancelFn: (() => void) | undefined; let isCanceled = false; let resolveWait: (() => void) | undefined; const responsePromise = (async () => { // 等待可用的并发槽位 if (activeCount >= MAX_CONCURRENT) { await new Promise<void>(resolve => { resolveWait = resolve; waitingQueue.push(resolve); }); } if (isCanceled) { throw new Error('Request canceled'); } activeCount++; const request = helper.getArrayBuffer(requestParameters); cancelFn = request.cancel; try { const response = await request.response; if (isCanceled) throw new Error('Request canceled'); const image = await arrayBufferToCanvasImageSource(response.data); return { data: image, cacheControl: response.cacheControl, expires: response.expires }; } finally { activeCount--; // 从等待队列取出一个请求,允许它发起 if (waitingQueue.length > 0) { waitingQueue.shift()?.(); } // 如果当前请求被取消且还在等待队列,清理它 if (resolveWait && waitingQueue.includes(resolveWait)) { const idx = waitingQueue.indexOf(resolveWait); if (idx !== -1) waitingQueue.splice(idx, 1); } } })(); return { response: responsePromise, cancel() { isCanceled = true; cancelFn?.(); // 如果请求在等待队列,直接触发resolve让它进入执行阶段后抛出取消错误 resolveWait?.(); responsePromise.catch(() => {}); } }; }
关键逻辑说明
activeCount:实时跟踪当前正在运行的请求数,超过最大值时,新请求进入等待队列。waitingQueue:存储等待的请求的resolve函数,当有请求完成时,取出一个resolve,释放并发槽位。- 取消处理:如果请求在等待队列,直接resolve让它进入执行阶段,再通过
isCanceled拒绝;如果已经发起,直接调用cancel方法。
内容的提问来源于stack exchange,提问作者smellyshovel
相关产品推荐
相关产品推荐

