1

场景描述

  • 笔者的一个朋友,最近出去面试,被问到了如何控制并发请求
  • 笔者的朋友回答,浏览器自带并发控制为6,一般不需要控制
  • 包括其他的浏览器,一般并发数都是6(一次发6个,第7个等前6个发完再发-队列思想)
  • 面试官对笔者朋友的回答差强人意
  • 于是乎,本文就探讨一下js并发请求的控制

为何谷歌浏览器控制并发数是6?

  • 问:为何谷歌浏览器控制并发数是6?
  • 答:这是浏览器设计的规则
  • 问:从哪里能看到这个规则
  • 答:浏览器源码中能看到

谷歌浏览器源码下载

控制并发数6的谷歌浏览器源码

  • 经过很长时间查找,总算找到了为何并发数是6的原因
  • 如下截图:

大家感兴趣的话,也可以自行下载谷歌浏览器源码,去按照上图中文件位置,查看源码
  • 笔者认为,我们在写代码中,一般不用刻意控制并发请求的数量,因为浏览器帮我们做了兜底的控制
  • 比如大文件的分片上传操作,就是并发的多个请求
  • 浏览器兜底并发6个,如上图示例
  • 但是有的面试官还是希望我们能够进一步说说这个
  • 甚至是能够手写一个函数,用于控制并发请求

接下来,我们继续说一下,js中写一个控制并发请求的函数...

笔者提供后端请求接口

  • 以往的文章案例中,常常是代码不全(只有前端,没后端)
  • 不太好做到复制粘贴即用演示,对于读者略微不太友好
  • 为了方便大家更好的演示理解,笔者用自己的服务器,简单写了一个请求接口

简单接口

route.get('/getIdName', (req, res) => {
  res.header('Access-Control-Allow-Origin', '*');
  let key = req.query.id
  res.send({
    code: '0',
    message: '成功',
    data: {
      idName: '这个ID名字为: ' + key
    }
  })
})

请求示例

let urls = [
    'http://ashuai.work/api/getIdName?id=1',
    'http://ashuai.work/api/getIdName?id=2',
    'http://ashuai.work/api/getIdName?id=3',
    'http://ashuai.work/api/getIdName?id=4',
    'http://ashuai.work/api/getIdName?id=5',
    'http://ashuai.work/api/getIdName?id=6',
    'http://ashuai.work/api/getIdName?id=7',
    'http://ashuai.work/api/getIdName?id=8',
    'http://ashuai.work/api/getIdName?id=9',
    'http://ashuai.work/api/getIdName?id=10',
    'http://ashuai.work/api/getIdName?id=11',
    'http://ashuai.work/api/getIdName?id=12',
    'http://ashuai.work/api/getIdName?id=13',
    'http://ashuai.work/api/getIdName?id=14',
    'http://ashuai.work/api/getIdName?id=15',
    'http://ashuai.work/api/getIdName?id=16',
    'http://ashuai.work/api/getIdName?id=17',
    'http://ashuai.work/api/getIdName?id=18',
];

返回结果示例

get请求,直接url赋值地址栏粘贴回车即可,如:http://ashuai.work/api/getIdName?id=6

{"code":"0","message":"成功","data":{"idName":"这个ID名字为: 6"}}

并发请求控制,方案一:直接使用Promise.all

Promise.all并发请求代码

复制粘贴即用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>循环异步请求</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.0/axios.js"></script>
</head>
<body>
    <script>
        let urls = [
            'http://ashuai.work/api/getIdName?id=1',
            'http://ashuai.work/api/getIdName?id=2',
            'http://ashuai.work/api/getIdName?id=3',
            'http://ashuai.work/api/getIdName?id=4',
            'http://ashuai.work/api/getIdName?id=5',
            'http://ashuai.work/api/getIdName?id=6',
            'http://ashuai.work/api/getIdName?id=7',
            'http://ashuai.work/api/getIdName?id=8',
            'http://ashuai.work/api/getIdName?id=9',
            'http://ashuai.work/api/getIdName?id=10',
            'http://ashuai.work/api/getIdName?id=11',
            'http://ashuai.work/api/getIdName?id=12',
            'http://ashuai.work/api/getIdName?id=13',
            'http://ashuai.work/api/getIdName?id=14',
            'http://ashuai.work/api/getIdName?id=15',
            'http://ashuai.work/api/getIdName?id=16',
            'http://ashuai.work/api/getIdName?id=17',
            'http://ashuai.work/api/getIdName?id=18',
        ];

        Promise.all(urls.map(url => axios.get(url)))
            .then(responses => {
                let data = responses.map(response => response.data);
                console.log(data);
            })
            .catch(error => {
                console.error('报错啦: ', error);
            });

    </script>
</body>
</html>

效果图

  • Promise.all丢出去一堆请求
  • 它不关注,谁快谁慢
  • 所以这种方式并发请求,也是可以的,不过唯一的确定就是不保证按照顺序走的
  • 当然,就算是手写并发请求,也不一定完全保证请求顺序,除非并发请求设置单次执行(即一次并发1个请求)
一般来说,一些需求,前人都考虑到了,都会提供对应的npm包给我们使用,所以这里我们使用社区比较知名的并发请求的包:p-limit

什么是p-limit?

官方:Run multiple promise-returning & async functions with limited concurrency,Works in Node.js and browsers.

大白话:能够在node或者浏览器中使用的,控制并发的异步函数

是很优秀的一个npm包,周下载量过亿!

npm地址:https://www.npmjs.com/package/p-limit

使用p-limit进行并发控制

  • 冗余的代码语法规则,这里不赘述
  • 直接贴上完整代码(带注释)
  • 老规则,复制粘贴即用

代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.0/axios.js"></script>
</head>

<body>
    <script type="module">
        // 通过js模块方式在jsdelivr这个cdn中引入p-limit
        import pLimit from 'https://cdn.jsdelivr.net/npm/p-limit@5.0.0/+esm'

        const limit = pLimit(2); // 并发限制2个2个的发请求

        // 一堆请求数组,用Promise.all接收
        const input = [
            limit(async () => {
                let res = await axios.get('http://ashuai.work/api/getIdName?id=1')
                return res.data
            }),
            limit(async () => {
                let res = await axios.get('http://ashuai.work/api/getIdName?id=2')
                return res.data
            }),
            limit(async () => {
                let res = await axios.get('http://ashuai.work/api/getIdName?id=3')
                return res.data
            }),
            limit(async () => {
                let res = await axios.get('http://ashuai.work/api/getIdName?id=4')
                return res.data
            }),
            limit(async () => {
                let res = await axios.get('http://ashuai.work/api/getIdName?id=5')
                return res.data
            }),
            limit(async () => {
                let res = await axios.get('http://ashuai.work/api/getIdName?id=6')
                return res.data
            }),
            limit(async () => {
                let res = await axios.get('http://ashuai.work/api/getIdName?id=7')
                return res.data
            }),
            limit(async () => {
                let res = await axios.get('http://ashuai.work/api/getIdName?id=8')
                return res.data
            }),
        ];

        const result = await Promise.all(input);
        console.log(result);
    </script>
</body>

</html>

效果图

由下图可见,的确是两个两个的发请求

使用循环优化一下

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.0/axios.js"></script>
</head>

<body>
    <script type="module">
        // 通过js模块方式在jsdelivr这个cdn中引入p-limit
        import pLimit from 'https://cdn.jsdelivr.net/npm/p-limit@5.0.0/+esm'

        // 笔者提供的请求接口
        let urls = [
            'http://ashuai.work/api/getIdName?id=1',
            'http://ashuai.work/api/getIdName?id=2',
            'http://ashuai.work/api/getIdName?id=3',
            'http://ashuai.work/api/getIdName?id=4',
            'http://ashuai.work/api/getIdName?id=5',
            'http://ashuai.work/api/getIdName?id=6',
            'http://ashuai.work/api/getIdName?id=7',
            'http://ashuai.work/api/getIdName?id=8',
            'http://ashuai.work/api/getIdName?id=9',
            'http://ashuai.work/api/getIdName?id=10',
            'http://ashuai.work/api/getIdName?id=11',
            'http://ashuai.work/api/getIdName?id=12',
            'http://ashuai.work/api/getIdName?id=13',
            'http://ashuai.work/api/getIdName?id=14',
            'http://ashuai.work/api/getIdName?id=15',
            'http://ashuai.work/api/getIdName?id=16',
            'http://ashuai.work/api/getIdName?id=17',
            'http://ashuai.work/api/getIdName?id=18',
        ]

        const limit = pLimit(2);
        const input = []

        urls.forEach((url) => {
            let item = limit(async () => {
                let res = await axios.get(url)
                return res.data
            })
            input.push(item)
        })
        const result = await Promise.all(input);
        console.log(result);
    </script>
</body>

</html>

补充 p-queue

还是不少star的
  • 实际上,作为调包侠的我们,若是空闲了,还是要研究一下一些npm包的原理的
  • 接下来,我们手写一个队列
  • 面试的过程中,把这个答出来
  • 这个问题才算是回答的不错

自己手写一个并发请求函数

思路分析

  • 因为是要等到拿到所有的请求,所以要用到Promise的语法
  • 比如设置并发3个请求,那我们需要统计当前在跑的请求有没有达到3个
  • 没达到就继续执行请求(从任务数组中取出一项执行),达到了先缓一缓(while循环更加合适)
  • 当请求任务数组结束,并且也没有在跑的任务时
  • 说明活全部干完了
  • 再把结果resolve出去给外边
大致是这个思路,请看代码更加明晰

代码附上

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.0/axios.js"></script>
</head>

<body>
    <script>
        function concurrentReq(urls, limit) {
            return new Promise((resolve, reject) => {
                let num = 0; // 当前在跑的请求数量(在跑的请求数量要小于限制的数量)
                let task = urls; // 并发任务数组
                let results = []; // 最终并发请求结果存放的数组
                // 递归闭包函数调用发请求,Promise返回最终结果
                function goNext() {
                    if (task.length === 0 && num === 0 ) {
                        // 当没有更多任务且没有请求正在进行时
                        resolve(results); // 所有请求已完成,resolve吐出去返回结果
                        return;
                    }
                    while (num < limit && task.length > 0) { // 当请求任务小于3且还有任务继续干时,goNext
                        let url = task.shift(); // 把并发任务数组中第一项剔除掉,并拿到第一项(请求接口)
                        num = num + 1 // 并记录一下当前的请求数量
                        axios.get(url) // 发请求
                            .then((res) => {
                                num = num - 1 // 请求成功,就把计数减一
                                results.push(res.data); // 把请求的结果依次存起来
                                goNext(); // 递归调用,继续执行下一个请求任务
                            })
                            .catch((err) => {
                                num = num - 1 // 请求失败,也把计数减一
                                console.error(`此接口:${url}请求失败,报错信息:${err}`);
                                goNext(); // 递归调用,继续执行下一个请求任务
                            })
                    }
                }
                goNext();
            });
        }

        // 笔者提供的请求接口
        let urls = [
            'http://ashuai.work/api/getIdName?id=1',
            'http://ashuai.work/api/getIdName?id=2',
            'http://ashuai.work/api/getIdName?id=3',
            'http://ashuai.work/api/getIdName?id=4',
            'http://ashuai.work/api/getIdName?id=5',
            'http://ashuai.work/api/getIdName?id=6',
            'http://ashuai.work/api/getIdName?id=7',
            'http://ashuai.work/api/getIdName?id=8',
            'http://ashuai.work/api/getIdName?id=9',
            'http://ashuai.work/api/getIdName?id=10',
            'http://ashuai.work/api/getIdName?id=11',
            'http://ashuai.work/api/getIdName?id=12',
            'http://ashuai.work/api/getIdName?id=13',
            'http://ashuai.work/api/getIdName?id=14',
            'http://ashuai.work/api/getIdName?id=15',
            'http://ashuai.work/api/getIdName?id=16',
            'http://ashuai.work/api/getIdName?id=17',
            'http://ashuai.work/api/getIdName?id=18',
        ]
        let limit = 3; // 假设控制并发请求数量每次发三个

        concurrentReq(urls, limit)
            .then((results) => {
                console.log('所有请求执行结果:', results);
            })
            .catch((error) => {
                console.error('请求执行出错:', error);
            });

    </script>
</body>

</html>

效果图

总结

  • 关于面试题如何控制js并发一百个请求?这个问题
  • 我们可以回答到几个关键字
  • 浏览器自带并发控制6个
  • 市面上一些npm包也可以直接拿来用
  • 当然自己也可以手写一个并发请求控制函数
  • 这样回答这道面试题,面试官应该会满意的

要是你这样回答,面试官依旧不满意,呃,那你就当我没说😅😅😅

A good memory is better than a bad pen. Write it down...

水冗水孚
1.1k 声望585 粉丝

每一个不曾起舞的日子,都是对生命的辜负