请求并发控制
面试问题浏览器有这样的一个并发请求封装这样的一个函数每次只能并发5到6个请求超过这个数量就踢掉一个请求要么就加入一个新的请求列表之类的其实我没太听懂这个题其实是在考请求并发控制也叫请求池 / 并发队列 / promise pool / concurrency limit浏览器对同一域名本身就有并发连接限制通常 HTTP/1.1 下常见是 6 个左右。但面试官让你“封装一个函数每次只能并发 5 到 6 个请求”不是让你依赖浏览器限制而是让你在代码层面实现一个任务调度器。它到底想解决什么假设你有 100 个请求const urls [ /api/1, /api/2, /api/3, // ... /api/100 ]如果直接Promise.all(urls.map(url axios.get(url)))它会几乎同时把 100 个请求都发出去。更好的方式是最多同时发 5 个 有一个完成了再补上下一个 一直到所有请求完成过程类似开始1 2 3 4 5 正在请求 请求 2 完成 - 立刻发起 6 请求 4 完成 - 立刻发起 7 请求 1 完成 - 立刻发起 8 ...这就是“加入一个新的请求列表”那部分。基础版限制最大并发数function requestPool(tasks, limit 5) { const results [] let index 0 let running 0 return new Promise((resolve, reject) { function runNext() { if (index tasks.length running 0) { resolve(results) return } while (running limit index tasks.length) { const currentIndex index const task tasks[index] index running Promise.resolve(task()) .then(res { results[currentIndex] res }) .catch(err { results[currentIndex] err }) .finally(() { running-- runNext() }) } } runNext() }) }使用方式const tasks urls.map(url { return () axios.get(url) }) const res await requestPool(tasks, 5)注意这里传进去的不是 Promise而是函数() axios.get(url)因为 Promise 一旦创建就已经开始执行了。我们要控制什么时候发请求所以必须传“函数”。如果某个请求失败要不要中断上面的版本是失败也不中断继续跑后面的请求如果你希望失败就整体 reject可以改成function requestPool(tasks, limit 5) { const results [] let index 0 let running 0 let stopped false return new Promise((resolve, reject) { function runNext() { if (stopped) return if (index tasks.length running 0) { resolve(results) return } while (running limit index tasks.length) { const currentIndex index const task tasks[index] index running Promise.resolve(task()) .then(res { results[currentIndex] res }) .catch(err { stopped true reject(err) }) .finally(() { running-- runNext() }) } } runNext() }) }“超过数量就踢掉一个请求”是什么意思这里可能有两种理解。1. 常规理解不是踢掉而是排队大多数场景是这样最多并发 5 个 超过 5 个的请求先进入等待队列 前面的请求完成一个再从队列里取一个执行这个就是上面的requestPool。2. 真的踢掉取消最早的请求或拒绝新请求有些业务场景比如搜索框联想、地图拖动、实时筛选确实可能需要如果当前已经有 5 个请求再来一个新请求就取消最旧的那个这个要配合AbortController。示例class RequestLimiter { constructor(limit 5) { this.limit limit this.running [] } request(url, options {}) { if (this.running.length this.limit) { const oldest this.running.shift() oldest.controller.abort() } const controller new AbortController() const task fetch(url, { ...options, signal: controller.signal }).finally(() { this.running this.running.filter(item item.controller ! controller) }) this.running.push({ controller, task }) return task } }使用const limiter new RequestLimiter(5) limiter.request(/api/a) limiter.request(/api/b) limiter.request(/api/c)当第 6 个请求进来时会取消最早那个请求。不过这种“踢掉请求”的策略不是通用方案要看业务。很多接口不能随便取消比如保存、支付、提交表单。面试里推荐怎么答你可以这样说这个问题是请求并发控制。前端可以封装一个请求池维护两个变量当前正在执行的请求数量和等待执行的请求队列。每次发请求时如果当前并发数小于限制比如 5就立即执行如果已经达到限制就把请求函数放进队列。每当一个请求完成就把并发数减一然后从队列里取下一个请求继续执行。这样可以保证任意时刻最多只有 5 个请求在执行。然后补一句如果业务要求“超过数量踢掉一个请求”可以结合 AbortController达到上限时取消最早的请求或者拒绝最新请求。但普通批量请求一般是排队而不是取消。这就很完整了。最好记的简版代码function limitRequest(tasks, limit 5) { const results [] let index 0 let running 0 return new Promise(resolve { function next() { if (index tasks.length running 0) { resolve(results) return } while (running limit index tasks.length) { const i index running tasks[i]() .then(res { results[i] res }) .catch(err { results[i] err }) .finally(() { running-- next() }) } } next() }) }使用const tasks urls.map(url () fetch(url)) const result await limitRequest(tasks, 5)记住一句话就行控制并发不是控制 Promise而是控制 Promise 什么时候被创建。所以传入的一定是() fetch(url)而不是fetch(url)