面试官:前端如果 100 个请求,你怎么用 Promise 去控制并发?

news2025/1/15 6:19:22

在这里插入图片描述
摘要:

时隔两年半,我,一个卑微的前端菜鸡,又来写面经了!以为钱是程序员年轻奋斗的动力!作为一个程序员,在一个地方慢慢成长后会产生一个能力小提升的一种傲娇!希望你们一跳涨好几丈。。。下面是我最近面试遇到的题目,总结了一下。。。

由于js是单线程的,并不存在真正的并发,但是由于JavaScript的Event Loop机制,使得异步函数调用有了“并发”这样的假象

题目:

// 设计一个函数,可以限制请求的并发,同时请求结束之后,调用callback函数
sendRequest(requestList: , limits, callback): voidsendRequest([
		() => request('1'), 
		() => request('2'), 
		() => request('3'), 
		() => request('4')],3, //并发数
		(res)=>{
			console.log(res)
}) 
function request (url,time=1){
		return new Promise((resolve,reject)=>{
			setTimeout(()=>{
				console.log('请求结束:'+url);
				if(Math.random() > 0.5){
					resolve('成功')
				}else{
					reject('错误;')
				}
			},time*1e3)
		})
}

概念理解:串发和并发

串行: 一个异步请求完了之后在进行下一个请求!从上到下依次执行对应接口请求

var p = function () {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log("1000");
      resolve();
    }, 1000);
  });
};
var p1 = function () {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log("2000");
      resolve();
    }, 2000);
  });
};
var p2 = function () {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log("3000");
      resolve();
    }, 3000);
  });
};

p()
  .then(() => {
    return p1();
  })
  .then(() => {
    return p2();
  })
  .then(() => {
    console.log("end");
  });

并行: 多个异步请求同时进行!

var promises = function () {
  return [1000, 2000, 3000].map((current) => {
    return new Promise(function (resolve, reject) {
      setTimeout(() => {
        console.log(current);
      }, current);
    });
  });
};

Promise.all(promises()).then(() => {
  console.log("end");
});

Promise.all可以保证,promises数组中所有promise对象都达到resolve状态,才执行then回调。

场景1:
假设以下有 30 个异步请求需发送,但由于某些原因,我们必须将同一时刻并发请求数量控制在 5 个以内,同时还要尽可能快速的拿到响应结果。

如图所示:
在这里插入图片描述
图中这样的排队和并发请求的场景基本类似,窗口只有三个,人超过三个之后,后面的人只能排队了!

场景2:

如果你的promises数组中每个对象都是http请求,而这样的对象有几十万个。那么会出现的情况是,你在瞬间发出几十万个http请求,这样很有可能导致堆积了无数调用栈导致内存溢出。这时候,我们就需要考虑对Promise.all做并发限制。Promise.all并发限制指的是,每个时刻并发执行的promise数量是固定的,最终的执行结果还是保持与原来的Promise.all一致。

整体采用递归调用来实现:最初发送的请求数量上限为允许的最大值,并且这些请求中的每一个都应该在完成时继续递归发送,通过传入的索引来确定了urls里面具体是那个URL,保证最后输出的顺序不会乱,而是依次输出。

function multiRequest(urls, maxNum) {
 const len = urls.length; // 请求总数量
 const res = new Array(len).fill(0); // 请求结果数组
 let sendCount = 0; // 已发送的请求数量
 let finishCount = 0; // 已完成的请求数量
 return new Promise((resolve, reject) => {
     // 首先发送 maxNum 个请求,注意:请求数可能小于 maxNum,所以也要满足条件2
     // 同步的 创建maxNum个next并行请求 然后才去执行异步的fetch 所以一上来就有5个next并行执行
     while (sendCount < maxNum && sendCount < len) { 
         next();
     }
     function next () {
         let current = sendCount ++; // 当前发送的请求数量,后加一 保存当前请求url的位置
         // 递归出口
         if (finishCount >= len) { 
         // 如果所有请求完成,则解决掉 Promise,终止递归
             resolve(res);
             return;
         }
         const url = urls[current];
         fetch(url).then(result => {
             finishCount ++;
             res[current] = result;
             if (current < len) { // 如果请求没有发送完,继续发送请求
                 next();
             }
         }, err => {
             finishCount ++;
             res[current] = err;
             if (current < len) { // 如果请求没有发送完,继续发送请求
                 next();
             }
         });
     }
 });
}

await实现:

async function sendRequest(requestList,limits,callback){
	// 维护一个promise队列    
	const promises = [] // 当前的并发池,用Set结构方便删除    
	const pool = new Set()
	//set也是Iterable<any>[]类型,因此可以放入到race里
	// 开始并发执行所有的任务    
	for(let request of requestList){// 开始执行前,先await 判断 当前的并发任务是否超过限制        
		    if(pool.size >= limits){// 这里因为没有try catch ,所以要捕获一下错误,不然影响下面微任务的执行            
				  await Promise.race(pool).catch(err=>err)        
		    }       
			const promise = request()// 拿到promise,删除请求结束后,从pool里面移除        
			const cb = ()=>{
				  pool.delete(promise)        
			}// 注册下then的任务        
			promise.then(cb,cb)
			pool.add(promise)
			promises.push(promise)    
	}// 等最后一个for await 结束,这里是属于最后一个 await 后面的 微任务    
	 // 注意这里其实是在微任务当中了,当前的promises里面是能确保所有的promise都在其中(前提是await那里命中了if)    
	Promise.allSettled(promises).then(callback,callback)
}

总结:

  1. race的特性可找到并发任务里最快结束的请求
  2. 用for await 可保证for结构体下面的代码是最后await后的微任务,而在最后一个微任务下,可保证所有的promise已经存入promises里(如果没命中任何一个await,即限制并发数>任务数的时候,虽然不是在微任务当中,也可以保证所有的promise都在里面),最后利用allSettled,等待所有的promise状态转变后,调用回调函数
  3. 并发任务池用Set结构存储,可通过指针来删除对应的任务,通过闭包引用该指针从而达到 动态控制并发池数目
  4. for await 结构体里,其实await下面,包括结构体外都是属于微任务(前提是有一个await里面的if被命中),至于这个微任务什么时候被加入微任务队列,要看请求的那里的在什么时候开始标记(resolve/reject
  5. for await 里其实已经在此轮宏任务当中并发执行了,await后面的代码被挂起来,等前一个promise转变状态–>移出pool–>将下一个promise捞起加入pool当中 -->下一个await等待最快的promise,如此往复。

写一个方法,实现最大并发数的并发请求

function multiRequestLimitNum (reqArr, limitNum) {
    const reqLen = reqArr.length
    const resArr = new Array(reqLen)
    let i = 0
    return new Promise((resolve, reject) => {
        const maxNum = reqLen >= limitNum ? limitNum : reqLen
        while (i < maxNum) {
            reqFn()
        }
        async function reqFn () {
            if (i > reqLen - 1) return
            const cur = i++
            const fn = reqArr[cur]
            const data = await fn().catch((err) => { return err })
            resArr[cur] = data
            // 不用length判断,因为resArr里面是empty,用Object.values
            if (i === Object.values(resArr).length) resolve(resArr)
            else reqFn()
        }
    })
}
function req (res, delay) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(res)
    }, delay)
  })
}
multiRequestLimitNum([
  req.bind(null, 1, 1000),
  req.bind(null, 2, 3000),
  req.bind(null, 3, 2000),
  req.bind(null, 4, 100)],
2)

头条前端笔试题 - 实现一个带并发限制的promise异步调度器

在这里插入图片描述

上面正常执行是3421,控制并发为后执行是2314

实现思路:

  1. 先把要执行的promise function 存到数组内
  2. 最多执行为2,那我们必然是要启动的时候就要让两个promise函数执行
  3. 设置一个临时变量,表示当前执行ing几个promise
  4. 然后一个promise执行完成将临时变量-1
  5. 然后借助递归重复执行
function Scheduler(){
    this.list=[]
    this.add=function(promiseCreator){
       this.list.push(promiseCreator)
    }

    this.maxCount=2;
    
    var tempRunIndex=0;

    this.taskStart=function(){
       for(var i =0 ;i<this.maxCount;i++){
           request.bind(this)()
       }
    }

    function request(){

        if(!this.list || !this.list.length || tempRunIndex>=this.maxCount){
            return
        }

        tempRunIndex++
        this.list.shift()().then(()=>{
            tempRunIndex--
            request.bind(this)()
        })
    }
}

function timeout(time){
    return new Promise(resolve=>{
        setTimeout(resolve,time)
    })
}

var scheduler = new Scheduler()

function addTask(time,order){
    scheduler.add(()=>timeout(time).then(()=>console.log(order)))
}


addTask(1000,1)
addTask(500,2)
addTask(300,3)
addTask(400,4)

scheduler.taskStart()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/540820.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【问题总结】不使用回滚,如何删除/剔除/回退 远程仓库的错误commit。

场景描述 某次使用IDEA操作GIT时&#xff0c;将一些【被忽略】的文件都提交到commit中&#xff0c;并且被push到远程仓库&#xff0c;甚至还被合并到了主分支中该怎么办&#xff1f; 解决思路 分享两种思路 删除/回退/剔除 掉远程错误的分支重新commit一次正确的分支 删除…

企业落地数字化转型,如何部署战略规划

当前环境下&#xff0c;各领域企业通过数字化相关的一切技术&#xff0c;以数据为基础、以用户为核心&#xff0c;创建一种新的&#xff0c;或对现有商业模式进行重塑就是数字化转型。这种数字化转型给企业带来的效果就像是一次重构&#xff0c;会对企业的业务流程、思维文化、…

降低 Spark 计算成本 50.18 %,使用 Kyligence 湖仓引擎构建云原生大数据底座,为计算提速 2x

2023 中国开源未来发展峰会于 5 月 13 日成功举办。在大会开源原生商业分论坛&#xff0c;Kyligence 解决方案架构高级总监张小龙发表《云原生大数据底座演进 》主题演讲&#xff0c;向与会嘉宾介绍了他对开源发展的见解&#xff0c;数据底座向云原生湖仓一体架构演进的趋势&am…

建立在Safe生态的—GameFi SocialFi双赛道项目No.1头号玩家

最近大家关注的重点在BRC-20和MEME项目&#xff0c;人们似乎更在意短期的投机回报。而在这之外&#xff0c;一个web3的游戏——No.1头号玩家却得到了大量的玩家支持。 据了解&#xff0c;No.1是一个GameFi & SocialFi的双赛道web3游戏&#xff0c;中文名称为头号玩家。它是…

系统分析师上午题,经典易错题目

CRC即循环冗余校验码(Cyclic Redundancy Check)是数据通信领域中最常用的一种差错校验码,其特征是信息字段和校验字段的长度可以任意选定。在CRC校验方法中,进行多项式除法(模2除法)运算后的余数为校验字段。第一个空的分析,系统读记录的时间为33/11=3ms,对第一种情况:…

计算机毕业论文选题推荐|软件工程|系列七

文章目录 导文题目导文 计算机毕业论文选题推荐|软件工程 (***语言)==使用其他任何编程语言 例如:基于(***语言)门窗账务管理系统的设计与实现 得到:基于JAVA门窗账务管理系统的设计与实现 基于vue门窗账务管理系统的设计与实现 等等 题目 基于(***语言)的家政服务系统…

Android 打开webView黑屏闪烁问题排查

______ NO.1 ______ 前言 最近在研发项目的时候&#xff0c;有个模块调用webView功能&#xff1b; 点击搜索框&#xff0c;进入webView页面&#xff0c;出现了黑色过渡页面&#xff0c;效果如下&#xff1a; ______ NO.2 ______ 排查问题 个人在排查此问题的时候&#xff0c;用…

Redis缓存双写一致性之更新策略

Redis缓存双写一致性之更新策略 一 面试题引入二 缓存双写一致性三 双写双检加锁策略四 数据库和缓存一致性的集中更新策略4.1 最终一致性4.2 可以关机的情况下4.3 不能关机的情况下&#xff0c;四种更新策略4.3.1 先更新数据库&#xff0c;再更新缓存4.3.2 先更新缓存&#xf…

【算法学习系列】03 - 由[1-5]等概率随机实现[2-10]等概率随机

文章目录 约定条件说明解决方案构造 0 1 发生器函数 f2()计算需要几个二进制位验证 2-10 等概率返回某个整数 总结 约定条件说明 假定 f() 是一个函数&#xff0c;保证 [1, 5] 范围内等概率返回一个整数实现 2-10 等概率随机不能使用 Math.random() 函数&#xff0c;只能使用函…

栈与队列的性质互换

本期内容&#xff1a;栈&#xff0c;队列的定义性质&#xff0c;性质转换 栈&#xff0c;队列的定义性质&#xff0c;性质转换 认识栈实现栈 队列实现 性质转换 认识栈 栈&#xff08;stack&#xff09;又名堆栈&#xff0c;它是一种运算受限的线性表。限定仅在表尾进行插入和…

【渗透测试】web日志、linux命令、常用知识

文章目录 web日志分析基础知识1. 编码2. 解码工具3. 数据提交方式4. 常见脚本语言5. 日志还原 分析日志1. 分析日志的目的2. 攻击出现的位置3. 攻击常见的语句4. 攻击常见的特点5. 攻击日志分析流程 相关linux命令常用命令系统状态检测命令工作目录切换命令文本文件编辑命令文件…

BlueZ自动连接蓝牙耳机

问题&#xff1a;调好蓝牙之后&#xff0c;出现了一个客户问题&#xff0c;第一次连接好之后&#xff0c;开关机后没法自动连了。 解决方法&#xff1a; 针对这个情况&#xff0c;实际定位一下问题原因&#xff0c;原来是蓝牙耳机每次连时&#xff0c;都要求授权服务: Author…

sqlmap

1、Sqlmap简介&#xff1a; Sqlmap是一个开源的渗透测试工具&#xff0c;可以用来自动化的检测&#xff0c;利用SQL注入漏洞&#xff0c;获取数据库服务器的权限。它具有功能强大的检测引擎&#xff0c;针对各种不同类型数据库的渗透测试的功能选项&#xff0c;包括获取数据库…

Maven安装和配置(详细版)

Maven安装和配置 Maven安装1、安装链接&#xff1a;2、配置环境变量&#xff1a; Maven配置1、修改Maven仓库下载镜像及修改仓库位置&#xff1a;2、在Idea上配置Maven&#xff1a; 测试Maven安装能否安装jar包 Maven安装 1、安装链接&#xff1a; Maven – Download Apache …

使用A100 GPU搭建OBBDetection的运行环境

项目场景&#xff1a; 最近需要复现一篇目标检测论文的代码&#xff0c;文章提供了代码&#xff0c;因此自己根据仓库的说明尝试配置环境运行代码&#xff0c;但遇到了非常多的困难 问题描述 比较老的代码加上比较的GPU&#xff0c;导致了环境在配置的时候困难重重 OBBDetect…

xorm多表连接查询

SQL的连接查询可以将多个表的数据查询出来&#xff0c;形成一个中间表。在sql中为JOIN关键字。最常用的是LEFT JOIN,RIGHT JOIN,INNER JOIN,OUTER JOIN。 xorm框架是基于go语言的orm框架同样支持连接查询&#xff0c;由于xom及支持原生的sql查询也支持基于xorm的方法查询&…

openEuler用户软件仓(EUR)| 近期项目介绍

在操作系统的世界&#xff0c;软件包是一等公民&#xff0c;软件包的丰富程度和是否易于分发&#xff0c;一定程度上决定了操作系统用户和开发者的使用体验.。 EUR(openEuler User Repo)是openEuler社区针对开发者推出的个人软件包托管平台&#xff0c;目的在于为开发者提供一个…

【LeetCode训练营】用栈来实现队列+用队列来实现栈 详解

&#x1f4af; 博客内容&#xff1a;【LeetCode训练营】用栈来实现队列用队列来实现栈 详解 &#x1f600; 作  者&#xff1a;陈大大陈 &#x1f680; 个人简介&#xff1a;一个正在努力学技术的准前端&#xff0c;专注基础和实战分享 &#xff0c;欢迎私信&#xff01; …

Requests-翻页请求实现

翻页请求实现 继https://blog.csdn.net/ssslq/article/details/130747686之后&#xff0c;本篇详述在获取了页面第一页之后&#xff0c;如何获取剩余页的标题内容。 网页&#xff1a;https://books.toscrape.com 找规律 同样还是进行页面的检查&#xff0c;切到网络一栏&…

MySQL查询——joininunion

MySql多表查询的几种方法 连接查询——join自连接查询子查询——&#x1f6e0;in合并查询——Union 认识MySQL数据库的多表查询&#xff0c;在对大量数据进行查询时仅仅使用一些基本的SQL语句已经无法满足我们日益增长的需求&#xff0c;如果要对多表进行查询就不得不认识以下几…