手把手系列,带你一步一步实现一个控制并发数量的函数

357 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

如果现在有一批等待发起请求的异步函数(http request),该怎么去处理?

一般思路: 将所有函数封装成 Promise,放入一个数组中,然后利用 Promise.all 发起请求

    let fns = [fn1, fn2, fn3, fn4];

    Promise.all(fns).then(res => {
        // ...
    })

曾经调试过这么一个问题,大概业务场景就是:

有一个ids list,需要分别调用详情接口去获取每个id的详细结果

有一天,这个 ids 长度达到了三位数;一些低端的手机,页面就怎么也打不开了...

好吧,线上有问题,那就 debugger 呗;

老步骤:找到这个页面的地址 👉 使用 Chrome 打开页面 👉 F12 开始调试

说实话,当这个页面的基本Html结构渲染完成,开始运行 js 之后,就能感觉到这个页面的不一般~~ 那不是一般的卡

打开 NetWork 面板,好家伙,一整页(甚至还有下一页)的接口都在 pending

然后就看到类似下面的代码 👇


    // 请求详情信息
    function requestDetailInfo(id) {
        fetch(`${BASE_URL}/getDetail?id=${id}`)
        .then((response) => {
            // ...
        })
    }

    const ids = [...ids];
    ids.forEach(id => requestDetailInfo(id));

着实让人感到汗颜,难道不知道浏览器对于同域名同一时间发起的请求数量是有限制的么?

一般常用的 Chrome 浏览器,对于同域名同一时间发起的请求数量限制在 6

控制一下函数的并发数量?

好吧,这么常见的问题,当然要写一个通用方法来处理一下了。

方案1 控制通道数量

思路:

1、用两个变量,一个为当前已经结束的请求数量 currentCount,一个为当前最大可用的并发数量 maxCount通道

2、当 currentCount 比需要请求的函数列表长度小,并且 maxCount > 0 时,从函数列表中找出一个待请求的任务开始执行,同时将 maxCount--

3、当函数执行完成后,将 通道currentCount 分别 + 1

4、当 currentCount 和 需要请求的函数列表长度 相等的时候,结束,返回结果

    // 定义状态
    const WAIT_STATE = 'wait';
    const ING_STATE = 'ing';
    const DONE_STATE = 'done';

    /**
     * 
     * @param {Array} list Array[item] item => {options, handle}
     * @param {Number} max 最大并发数量
     * @param {Function} callback 回调函数
     * @returns ans  按顺序返回结果
     */
    const requestLimit = (list, max = 6, callback) => {
        return new Promise((resolve, reject) => {
            try {
                // 当前已经结束的数量
                let currentCount = 0;
                // 最大通道数量
                let maxCount = max;
                const ans = [];
                // 重新初始化函数列表,并初始化函数状态
                const sequence = [...list].map(item => {
                    item._state = WAIT_STATE;
                    return item;
                });
                // 函数列表长度
                const len = sequence.length;

                // 启动函数
                const _start = () => {
                    // 当 `currentCount` 比需要请求的函数列表长度小,并且 `maxCount > 0` 时,从函数列表中找出一个待请求的任务开始执行
                    while(currentCount < len && maxCount > 0) {
                        // 将通道数量减1
                        maxCount--;
                        // 找到一个等待请求的函数
                        let i = sequence.findIndex(item => item._state == WAIT_STATE);
                        if(i == -1) return;

                        // 更改这个函数的状态为 ING_STATE
                        sequence[i]._state = ING_STATE;

                        // 利用 Promise.resolve 包装,执行函数
                        Promise.resolve(sequence[i].handle(sequence[i].options)).then(res => {
                            // 按顺序保存函数结果
                            ans[i] = res;
                        },err => {
                            // 按顺序保存函数报错信息
                            ans[i] = err;
                        }).finally(() => {
                            // 更改函数状态为 DONE_STATE
                            sequence[i]._state = DONE_STATE;

                            // 将 `通道` 和 `currentCount` 分别 + 1
                            currentCount++;
                            maxCount++;

                            // 如果 函数都执行完成
                            if(currentCount == len) {
                                resolve(ans);
                                typeof callback === 'function' && callback(ans);
                            } else {
                                // 否则再启动一次函数
                                _start();
                            }
                        });
                    }
                }
                // 初次启动函数
                _start();
            } catch (e) {
                // 返回报错信息
                reject(e)
            }
        })

    }

测试:

    const urls =[
        {info:'1', time:2000},
        {info:'2', time:1000},
        {info:'3', time:2000},
        {info:'4', time:2000},
        {info:'5', time:3000},
        {info:'6', time:1000},
        {info:'7', time:2000},
        {info:'8', time:2000},
        {info:'9', time:3000},
        {info:'10', time:1000}
    ]

    function loadImg(url){
        return new Promise((reslove, reject)=>{
            console.log(url.info + '---start')
            setTimeout(()=>{
                console.log(url.info, 'ok!!!')
                reslove(url.info)
            }, url.time)
        })
    }

    // 构造函数列表
    const list = urls.map(item => {
        return {
            options: item,
            handle: loadImg
        }
    });

    requestLimit(list, 3, ans => {
        console.log('all is end,result is :', ans)
    })

    // 输出
    // 1---start
    // 2---start
    // 3---start
    // 2 ok!!!
    // 4---start
    // 1 ok!!!
    // 5---start
    // 3 ok!!!
    // 6---start
    // 4 ok!!!
    // 7---start
    // 6 ok!!!
    // 8---start
    // 5 ok!!!
    // 9---start
    // 7 ok!!!
    // 10---start
    // 8 ok!!!
    // 10 ok!!!
    // 9 ok!!!
    // all is end,result is : [
    // '1',  '2', '3',
    // '4',  '5', '6',
    // '7',  '8', '9',
    // '10'
    // ]

方案2 利用 Promise.race

待更新