拒绝死记硬背!清晰思路讲透 控制并发数、Promise.all、Promise.race 的实现逻辑

🙌 如文章有误,恳请评论区指正,谢谢!
❤ 写作不易,「点赞」+「收藏」+「转发」 谢谢支持!

背景

近期求职季刷到非常多关于手写代码题,在研究 Promise 的相关特性过程中,发现其实背后涉及的原理是一样的。因此为了帮助大家更好地理解 Promise.allPromise.race控制 Promise 请求并发数 等等的题目,特地写下这篇文章~

前置代码

我先给出一段代码,大家可以先花5~10分钟来想想这段代码是为了实现上述目标中的哪一个。当你思考过程中遇到卡点或疑问时,我接下来的解析就能更好地帮助你来掌握:

let time1;

function gets (ids, max) {

  return new Promise((resolve) => {
    const res = new Array(ids.length).fill(null);
    let loadcount = 0;
    let curIndex = 0;

    function load (id, index) {
      return get(id).then(
        (data) => {
          console.log("完成一个", data);
          loadcount++;

          if (loadcount > ids.length) {
            // 越界的就不赋值了
            console.log("溢出的其他几个不理了");
          } else if (loadcount === ids.length) {
            res[index] = data; // 最后一个也返回了!
            console.log("收工");
            resolve(res);
          } else {
            res[index] = data;
            curIndex++;
            load(ids[curIndex], curIndex);
          }
        },
        (err) => {
          res[index] = err;
          loadcount++;
          curIndex++;
          if (loadcount < ids.length)
            load(ids[curIndex], curIndex);
        }
      );
    }

    for (let i = 0; i < max && i < ids.length; i++) {
      curIndex = i;
      load(ids[i], i);
    }
  });
}

function get (id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(id);
    }, 1000);
  })
}

time1 = new Date();
gets([101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112], 3).then(data => {
  let time2 = new Date();

  console.log("耗时", time2 - time1);
  console.log("返回数据", data, data.length);
})

初次看,是否难以理解其中的 gets 函数是否这样设计?没关系,看完我这篇文章,给你讲清楚核心,就轻松拿下!拒绝死记硬背!

Promise 核心

上述的代码实现的其实是 “控制 Promise 并发数” 的需求,那他具体是怎样实现的呢?
上面的 get 函数是模拟一个异步请求的场景,因此可以不用管,我们的主要精力放在 gets 函数即可。

核心参数用处介绍(重要!!!)

  1. loadcount 用于计算总共完成了多少个 Promise 请求了(成功 or 失败都算)
  2. curIndex 用于计算当前要处理的 Promise 请求是在数组中的第几位

gets 函数的入参和返回值

gets 函数是处理并发数的函数,如果一次性有30个 Promise 请求传入,而我限制一次性只能同时进行3个 Promise 并发,毫无疑问在30个请求最终都完成时,再统一返回。

因此入参如下:

  1. 要进行的 Promise 请求数组
  2. 控制并发的数量 num

返回的参数是一个 Promise 数组,里面装着各个 Promise 请求处理完的结果(无论成功 or 失败)

gets 函数的核心

gets 函数因为主要由2个部分构成,一个是 用于限制同时能开启多少条 Promise 处理队列。

for (let i = 0; i < max && i < ids.length; i++) {
  curIndex = i;
  load(ids[i], i);
}

你可以想象成 http1.1 的设计原理,http1.1 限制了 Chrome 浏览器一次性只能最多同时并发6条 TCP 连接,相当于同时开了6条流水线来进行工作,效率毫无疑问一下子多了数倍,且其中一两个流水线遭遇对头堵塞时,工作效率也不会受到太大影响。

而其中的 max 就是控制最大并发数,同时 i 又不能超过要处理的 Promise 最大长度(这个很好理解吧,我只有30个 Promise,你总不能蹦出来第31个 Promise 来处理吧)。

逻辑介绍

同时开 max 个并发队列,并置各自要处理的第一个 Promise 请求为 curIndex。例如:如果 max 是3,那么 for 循环执行三次,各自如下

curIndex = 0; // 第一个
load(ids[0], 0); // 处理第一个
// 其实 load 只传入一个 i 即可,只不过为了方便理解,我不仅传了i,还传了索引为i时要处理的是哪个 Promise 请求

curIndex = 1; // 第二个
load(ids[1], 1); // 处理第二个

curIndex = 2; // 第三个
load(ids[2], 2); // 处理第三个

此时就同时开启3个队列来处理接下来的全部30个 Promise 请求了。


另一个就是 Promise 执行函数 load,用于执行当前 Promise 请求并判断执行完成后是否到达 ids.length,如果没就继续递归 load 执行下一个,如果执行完了就返回最终 Promise 结果数组 res。

    function load (id, index) {
      return get(id).then(
        (data) => {
          console.log("完成一个", data);
          loadcount++;

          if (loadcount > ids.length) {
            // 越界的就不赋值了
            console.log("溢出的其他几个不理了");
          } else if (loadcount === ids.length) {
            res[index] = data; // 最后一个也返回了!
            console.log("收工");
            resolve(res);
          } else {
            res[index] = data;
            curIndex++;
            load(ids[curIndex], curIndex);
          }
        },
        (err) => {
          res[index] = err;
          loadcount++;
          curIndex++;
          
          if(loadcount < ids.length)
              load(ids[curIndex], curIndex);
        }
      );
    }
  1. 如果该 Promise 成功则进入 .then 的第一个部分,失败则第二个部分
  2. 每次完成先把 loadcount++,即完成数+1
  3. 此时看看完成数是否超过 ids.length 了,没超过就继续递归迭代
res[index] = data; // 结果数组保存一下 Promise 执行的结果
curIndex++; // 即将处理下一个索引的 Promise 请求
load(ids[curIndex], curIndex); // 递归!
  1. 如果完成数为 ids.length,那就任务完成可以 resolve 返回了!!!

可能有人有疑问,为什么要单独开一个 if (loadcount > ids.length) 的判断呢?

解答:肯定会“溢出”,因为你是同时开了3个并发队列,而当 loadcount === ids.length 只是其中一个队列执行完然后往后走时发现搞定了,就返回了,同时+1。但此时其他2个队列执行完后拿到的 loadcount 是已经+1后的,因此会出现 > 的情况。当然,如果你并发数只是1时,那就不用加 >

  1. 如果进入 error 的情况,处理逻辑一样,loadcount++,然后 curIndex++,然后看是否还没执行完,没执行完才继续递归!

触类旁通

到这里为止,“控制并发” 的核心逻辑就讲完了,那如果你看懂了上述的实现过程,此时类比去想想 Promise.all 或是 Promise.race 的实现思路是不是也是类似的,无非就是以下区别:

Promise.all

  1. 该函数介绍:Promise.all() 静态方法接受一个 Promise 可迭代对象数组作为输入,并返回一个 Promise。当所有输入的 Promise 都被兑现时,返回的 Promise 也将被兑现(即使传入的是一个空的可迭代对象),并返回一个包含所有兑现值的数组。如果输入的任何 Promise 被拒绝,则返回的 Promise 将被拒绝,并带有第一个被拒绝的原因。

  2. 实现思路:没有 max 这个参数来控制并发,同时开30个队列来执行 Promise 数组,当进入 .then 第一个参数即成功兑现时,继续按上面“控制并发”的逻辑走,直到所有都成功完成才全部一起 resolve 返回。但是如果有一个 Promise 结果进入 .then 第二个参数即失败时,此时立刻 resolve 返回(即不继续递归了),返回的内容是该失败的 Promise 请求的具体内容(即只返回一个 Promise

Promise.race

  1. 该函数介绍:Promise.race() 静态方法接受一个 Promise 可迭代对象数组作为输入,并返回一个 Promise。这个返回的 promise 会随着第一个 promise 的敲定而敲定

  2. 实现思路:没有 max 这个参数来控制并发,同时开30个队列来执行 Promise 数组,当进入 .then 第一个参数即成功兑现时,立刻 resolve 返回该结果(即不继续递归了);如果有一个 Promise 结果进入 .then 第二个参数即失败时,也是立刻 resolve 返回该结果(即不继续递归了)。

温馨提醒

只不过从上述的没有 max 这个参数来控制并发,我们可看出一次性传入的 Promise 数组不宜过大,不然同时开启太多肯定会导致一定的网络传输卡顿。

最后

我是 Smoothzjc,致力于产出更多且不仅限于前端方面的优质文章

大家也可以关注我的公众号 @ Smooth前端成长记录,及时通过移动端获取到最新文章消息!

写作不易,「点赞」+「收藏」+「转发」 谢谢支持❤

往期推荐

《手把手教前端从0到1通过 Node + Express 开发简易接口,项目开发+部署服务器(亲身痛苦经历)》

《都2022年了还不考虑来学React Hook吗?6k字带你从入门到吃透》

《一份不可多得的 Webpack 学习指南(1万字长文带你入门 Webpack 并掌握常用的进阶配置)》

《通过 React15 ~ 17 的优化迭代来简单聊聊 Fiber》

《【offer 收割机之面试必备】一篇非常全面的 从 URL 输入到页面展现的全过程 精华梳理》

《【offer 收割机之手写系列】10分钟带你掌握原理并手写防抖与节流的立即/非立即执行版本》

《【offer 收割机之 CSS 回顾系列】请你解释一下什么是 BFC ?他的应用场景有哪些?》

《Github + hexo 实现自己的个人博客、配置主题(超详细)》

《10分钟让你彻底理解如何配置子域名来部署多个项目》

《一文理解配置伪静态解决 部署项目刷新页面404问题

《带你3分钟掌握常见的水平垂直居中面试题》

《【建议收藏】长达万字的git常用指令总结!!!适合小白及在工作中想要对git基本指令有所了解的人群》

《浅谈javascript的原型和原型链(新手懵懂想学会原型链?看这篇文章就足够啦!!!)》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值