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

news/2024/5/9 6:54:13

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

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

由于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.lengthconst resArr = new Array(reqLen)let i = 0return new Promise((resolve, reject) => {const maxNum = reqLen >= limitNum ? limitNum : reqLenwhile (i < maxNum) {reqFn()}async function reqFn () {if (i > reqLen - 1) returnconst cur = i++const fn = reqArr[cur]const data = await fn().catch((err) => { return err })resArr[cur] = data// 不用length判断,因为resArr里面是empty,用Object.valuesif (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://wed.xjx100/news/198276.html

相关文章

HTB靶机013-Poison-WP

013-Poison 靶机IP&#xff1a; 10.10.10.84 Scan Nmap 快速扫描&#xff1a; ┌──(xavier㉿kali)-[~] └─$ sudo nmap -sSV -T4 10.10.10.84 -F Starting Nmap 7.93 ( https://nmap.org ) at 2023-04-30 16:41 CST Nmap scan report for 10.10.10.84 Host is up (0.27s…

数据库中的几项区别

mysql中in和exists区别 mysql中的in语句是把外表和内表作hash 连接&#xff0c;而exists语句是对外表作loop 循环&#xff0c;每次loop循环再对内表进行查询。一直大家都认为exists比in语句的效率要高&#xff0c;这种说法其实是不 准确的。这个是要区分环境的。 1. 如…

Netty核心组件模块(一)

1.Bootstrap和ServerBootstrap 1>.Bootstrap意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类; 2>.常见的方法有: ①.public ServerBootstr…

动态规划——带权活动选择

带权活动选择Time Limit: 3000 MSMemory Limit: 1000 KB Description 给定n个活动&#xff0c;活动ai表示为一个三元组(si,fi,vi)&#xff0c;其中si表示活动开始时间&#xff0c;fi表示活动的结束时间&#xff0c;vi表示活动的权重, si<fi。带权活动选择问题是选择一些活…

有意思的各类算法,思维题目分享

1.统计子矩阵 思路&#xff1a;二维前缀和超时&#xff0c;下面是前缀和加双指针&#xff0c;对列前缀和&#xff0c;两个玄幻控制行号&#xff0c;双指针控制列的移动 考查&#xff1a;前缀和双指针 import os import sys# 请在此输入您的代码 # 矩阵大小 N M n,m,kmap(int,…

用 CSS 自定义滚动条

简介 首先需要介绍一下滚动条的组成部分。滚动条包含 track 和 thumb&#xff0c;如下图所示&#xff1a; track是滚动条的基础&#xff0c;其中的 thumb是用户拖动支页面或章节内的滚动。 案例&#xff1a; 案例代码&#xff1a; <!DOCTYPE html> <html><he…

【C++】-模板初阶(函数和类模板)

作者&#xff1a;小树苗渴望变成参天大树 作者宣言&#xff1a;认真写好每一篇博客 作者gitee:gitee 作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作 者 点 点 关 注 吧&#xff01; 文章目录 前言一、为什么要模板&…

MindMaster思维导图及亿图图示会员 优惠活动

MindMaster思维导图及亿图图示会员 超值获取途径 会员九折优惠方法分享给大家&#xff01;如果有需要&#xff0c;可以上~ 以下是食用方法&#xff1a; MindMaster 截图 亿图图示 截图 如果需要MindMaster思维导图或者亿图图示会员&#xff0c;可按照如下操作领取超值折扣优惠…