前端面试题:实现批量请求数据,并控制请求并发数量,最后所有请求结束之后,执行callback回调函数

在这里插入图片描述

引子

想象一下排队打疫苗,

在这里插入图片描述
在这里插入图片描述
外面排了一堆人,接种点里面就那么几个可以打疫苗的地方,上图里面得有13个可以同时打疫苗的位置,诊室里面打完一个人,出来一个人,外面排队的人,排在最前面的再进去一个。

好了我们类比一下这道题,我们知道Chrome浏览器同时可以进行6个并行的请求任务,这里要让我们自定义最多可以同时进行多少请求,也就是请求的并发度, 那也就是说,如果现在给了我们20个请求,20个url需要去fetch,同时定义了,最多同时可以进行3个请求,那么一开始,我们先从头开始fetch, 第1个到到第3个,进行的很顺利,从第4个开始,因为并行数最多是3, 从第四个开始阻塞住了,也就是需要开始排队了,等前面3个请求,其中有任意一个结束了之后,第四个也就是排在阻塞队列最前面才能够接着进行请求。

解题思路

给我们一个url的数组,里面比如说有20个url,什么都不考虑,就是不考虑最多并发数和阻塞的情况下,我们直接for loop url的数组,然后一个个fetch就行了,

但是如果我们要引入最多并发数呢?
打疫苗我们知道要排队,因为接种点里面打疫苗的窗口就那么多,那么这里,我们怎么给请求fetch 排队呢?

这里我们需要借助一下Promis,

案例一:

new Promise((resolve, reject) => {
  resolve("success");  // 下面then 能够进行的前提是 我能够成功的执行resolve函数,
                       // 如果我执行reject函数,那么我们就会执行后面的catch部分内的函数
})
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.log(error);
  });

案例二:

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("success");   // 这里3000ms也就是3s之后, 成功的resolve之后,
                          // 下面then部分内的代码才能够正常的执行,否则就永远也不会执行到then里面的代码,
  }, 3000);
})
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.log(error);
  });

这两个案例一结合, 这不就是我们想要的排队功能嘛,每个都给我Promise 上,然后只要我不给你resolve,你就在那侯着,等我什么时候给你resolve,什么时候轮到你发送请求。

那么我们让请求可以等候了,但怎么排队呢?那就是借助一个数组,我们模仿下堆(Heap):队列优先,先进先出。 也就是如果当我们的请求从前到后,如果这时候比如说进行到第3个请求了,但是我们最多并发数就是3, 那么在进行到第四个请求的时候,我们就判断一下,现在已经进行的请求数是否超过最多并发数MAX值,超过了,那么我们就把 先前讲到的那个概念,新建一个Promise, 然后把里面的resolve函数 push进 我们创建的等待队列(数组)里面,后面的请求都同样处理,如果超过了,那么就放队列里面去,这里我们也是按顺序走的哈,

然后,我们再把注意力重新放回来最开始的几个请求,也就是没有超过最多并发数时候的,假设我们一个请求完成了,之后我们要查看下,(是不是后面还有人等着打疫苗呢?是的话,我们就叫一下,让他过来打疫苗)这里翻译过来就是我们的等候队列里面是不是还有没成功执行的resolve, 是的话,我们就执行下resolve函数,让那个进程的可以继续向下进行也就是发送请求,然后再队列shift一下,最前头的去掉,因为执行完了嘛,就别占位置了, 最后最后,再看下我们总共发送请求的数量,因为我们每次查看也都是在request执行完了之后采取判断查看,所以这时候我们再检查一下,已经发送了的请求数量,是否等于人家最开始给我们的url数组的长度,也就是里面url的数量,如果相等,那么证明我们请求都执行完了,我们此刻可以运行callback 函数了。

以上就是我简述的一个逻辑,具体的还要看下代码,下面上代码!

代码

实际代码

const allRequest = [
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=1",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=2",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=3",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=4",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=5",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=6",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=7",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=8",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=9",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=10",
];

function sendRequest(urls, max, callbackFunc) {
  const REQUEST_MAX = max;
  const TOTAL_REQUESTS_NUM = urls.length;
  const blockQueue = []; // 等待排队的那个队列
  let currentReqNumber = 0; // 现在请求的数量是
  let numberOfRequestsDone = 0; // 已经请求完毕的数量是
  const results = new Array(TOTAL_REQUESTS_NUM).fill(false); // 所有请求的返回结果,先初始化上

  async function init() {
    for (let i = 0; i < urls.length; i++) {
      request(i, urls[i]);
    }
  }

  async function request(index, reqUrl) {
    // 这个index传过来就是为了对应好哪个请求,
    // 放在对应的results数组对应位置上的,保持顺序
    if (currentReqNumber >= REQUEST_MAX) {
      await new Promise((resolve) => blockQueue.push(resolve)); // 阻塞队列增加一个 Pending 状态的 Promise,
      // 进里面排队去吧,不放你出来,不resolve你,你就别想进行下面的请求
    }
    reqHandler(index, reqUrl); // {4}
  }

  async function reqHandler(index, reqUrl) {
    currentReqNumber++; // {5}
    try {
      const result = await fetch(reqUrl);
      results[index] = result;
    } catch (err) {
      results[index] = err;
    } finally {
      currentReqNumber--;
      numberOfRequestsDone++;
      if (blockQueue.length) {
        // 每完成一个就从阻塞队列里剔除一个
        blockQueue[0](); // 将最先进入阻塞队列的 Promise 从 Pending 变为 Fulfilled,
        // 也就是执行resolve函数了,后面不就能继续进行了嘛
        blockQueue.shift();
      }
      if (numberOfRequestsDone === TOTAL_REQUESTS_NUM) {
        callbackFunc(results);
      }
    }
  }

  init();
}

sendRequest(allRequests, 2, (result) => console.log(result));

调试代码(比实际多了几个log()而已)

const allRequest = [
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=1",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=2",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=3",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=4",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=5",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=6",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=7",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=8",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=9",
  "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=10",
];

function sendRequest(urls, max, callbackFunc) {
  const REQUEST_MAX = max;
  const TOTAL_REQUESTS_NUM = urls.length;
  const blockQueue = [];                    // 等待排队的那个队列
  let currentReqNumber = 0;                 // 现在请求的数量是
  let numberOfRequestsDone = 0;             // 已经请求完毕的数量是
  const results = new Array(TOTAL_REQUESTS_NUM).fill(false); // 所有请求的返回结果,先初始化上

  async function init() {
    // {1} 
    for (let i = 0; i < urls.length; i++) {
      console.log("现在i是: " + i + " 正请求:" + urls[i]);
      request(i, urls[i]);
    }
  }

  async function request(index, reqUrl) {   // 这个index传过来就是为了对应好哪个请求,
                                            // 放在对应的results数组对应位置上的,保持顺序
    // {2}
    if (currentReqNumber >= REQUEST_MAX) {
      console.log(
        "currentReqNumber: " + currentReqNumber + 
        " ----REQUEST_MAX : " + REQUEST_MAX + 
        " ---- url: " + reqUrl);
      // {3}
      await new Promise((resolve) => blockQueue.push(resolve)); // 阻塞队列增加一个 Pending 状态的 Promise, 
                                                                // 进里面排队去吧,不放你出来,不resolve你,你就别想进行下面的请求
      console.log("第"+ i +"个请求等待结束: 即将开始执行:" + reqUrl);
    }
    reqHandler(index, reqUrl); // {4}
  }

  async function reqHandler(index, reqUrl) {
    currentReqNumber++; // {5}
    try {
      const result = await fetch(reqUrl);
      results[index] = result;
    } catch (err) {
      results[index] = err;
    } finally {
      currentReqNumber--;
      numberOfRequestsDone++;
      console.log(
        "done request: " +
          reqUrl +
          "  and currentReqNumber: " +
          currentReqNumber +
          "    .blockQueue.length => " +
          blockQueue.length
      );
      if (blockQueue.length) {
        // 每完成一个就从阻塞队列里剔除一个
        blockQueue[0](); // 将最先进入阻塞队列的 Promise 从 Pending 变为 Fulfilled
        blockQueue.shift();
        console.log(
          "消灭一个blockQueue第一个阻塞后,排队数为 : " + blockQueue.length
        );
      }
      if (numberOfRequestsDone === TOTAL_REQUESTS_NUM) {
        callbackFunc(results);
      }
    }
  }

  init();
}

sendRequest(allRequests, 2, (result) => console.log(result));

到此终于完事了,网上关于这道题的答案鱼龙混杂,错误的一堆,正确的没几个,所以在此特地加上自己的一丁点改写,传播正确的答案写法,以免错误的答案传播,并且浪费大家的时间,希望能帮到各位看到这的读者们!

如果您觉得写的还不错有帮到你,还望赏个赞,多谢啦! 如果写的不好的地方还望评论区指正,感谢!

最后非常感谢参考资料中第二个的那篇文章的分享者,其分享的文章中提到了原作者;
那个分享自微信公众号 - Nodejs技术栈(NodejsRoadmap),作者:五月君
原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

参考资料:
[1] JS请求并发数控制以及重发 题目图片粘贴自此
[2] 实现浏览器中的最大请求并发数控制

  • 11
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值