You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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(() => {});
        }
    };
}

关键逻辑说明

  1. requestChain:一个不断延伸的Promise链,每个新请求都会await requestChain,确保自己在所有之前的请求完成后才执行。
  2. 取消处理:
    • 用isCanceled标记请求状态,即使请求还在队列中未发起,也能直接拒绝Promise。
    • 如果请求已经发起,调用cancelFn取消网络请求。
  3. 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(() => {});
        }
    };
}

关键逻辑说明

  1. activeCount:实时跟踪当前正在运行的请求数,超过最大值时,新请求进入等待队列。
  2. waitingQueue:存储等待的请求的resolve函数,当有请求完成时,取出一个resolve,释放并发槽位。
  3. 取消处理:如果请求在等待队列,直接resolve让它进入执行阶段,再通过isCanceled拒绝;如果已经发起,直接调用cancel方法。

内容的提问来源于stack exchange,提问作者smellyshovel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 14:01:30