本文介绍在JavaScript和TypeScript中限制最大并发请求数的两种实现方式。第一种基于Promise.race机制,通过监控已完成Promise来动态加载新任务;第二种利用async/await与回调队列,当达到并发上限时挂起后续请求。文章包含完整代码示例及执行时间线分析,适用于需要控制网络请求并发量的场景。
场景
假设存在 10 个待处理的请求,但系统允许的最大并发执行数量仅为 5 个,且需要获取所有请求的最终结果。这是一个典型的并发请求控制场景。
模拟
首先,利用 setTimeout 函数来简单模拟一个异步请求过程。
let startTime = Date.now();
const timeout = (timeout: number, ret: number) => {
return (idx?: any) =>
new Promise((resolve) => {
setTimeout(() => {
const compare = Date.now() - startTime;
console.log(`At ${Math.floor(compare / 100)}00 return`, ret);
resolve(idx);
}, timeout);
});
};
const timeout1 = timeout(1000, 1);
const timeout2 = timeout(300, 2);
const timeout3 = timeout(400, 3);
const timeout4 = timeout(500, 4);
const timeout5 = timeout(200, 5);
这种模拟方式的本质是返回一个 Promise 对象。
没有并发控制的时候
const run = async () => {
startTime = Date.now();
await Promise.all([
timeout1(),
timeout2(),
timeout3(),
timeout4(),
timeout5(),
]);
};
run();
At 200 return 5
At 300 return 2
At 400 return 3
At 500 return 4
At 1000 return 1
观察输出结果,顺序为 5、2、3、4、1。这是根据各个请求设定的 timeout 时间长短直接输出的结果,未进行任何并发限制。
并发条件
假设同时间允许的最大并发数目为 2,我们需要创建一个类来管理这一限制。
class Concurrent {
private maxConcurrent: number = 2;
constructor(count: number = 2) {
this.maxConcurrent = count;
}
}
第一种并发控制
思路是将 Promise 数组按最大并发数进行拆分。当某个 Promise 状态变为 fulfilled(完成)时,将其从运行列表中移除,并从待处理队列中取出 pending 状态的 Promise 加入执行。Promise.race 方法可以帮助我们实现这一需求,即监听最先完成的 Promise。
class Concurrent {
private maxConcurrent: number = 2;
constructor(count: number = 2) {
this.maxConcurrent = count;
}
public async useRace(fns: Function[]) {
const runing: any[] = [];
// 按并发数,把 Promise 加进去
// Promise 会回调一个索引,方便我们知道哪个 Promise 已经 resolve 了
for (let i = 0; i < this.maxConcurrent; i++) {
if (fns.length) {
const fn = fns.shift()!;
runing.push(fn(i));
}
}
const handle = async () => {
if (fns.length) {
const idx = await Promise.race<number>(runing);
const nextFn = fns.shift()!;
// 移除已经完成的 Promise,把新的进去
runing.splice(idx, 1, nextFn(idx));
handle();
} else {
// 如果数组已经被清空了,表面已经没有需要执行的 Promise 了,可以改成 Promise.all
await Promise.all(runing);
}
};
handle();
}
}
const run = async () => {
const concurrent = new Concurrent();
startTime = Date.now();
await concurrent.useRace([timeout1, timeout2, timeout3, timeout4, timeout5]);
};
At 300 return 2
At 700 return 3
At 1000 return 1
At 1200 return 5
At 1200 return 4
可以看到输出顺序已经发生变化。为什么会这样?我们来分析一下,设定最大并发数为 2。
// 首先执行的是 1 和 2 1 需要 1000 MS 才执行完 2 需要 300 MS
当时间线到达 300 MS 时,2 执行完毕。此时移除 2,加入 3 并开始执行 3。3 需要 400 MS 执行完,时间线来到 700 MS。接着移除 3,加入 4 开始执行 4。4 需要 500 MS。当时间线来到 1000 MS 时,1 执行完毕,移除 1,加入 5 开始执行 5。最终在 1200 MS 时,4 和 5 刚好同时执行完毕。
第二种方案
可以利用 async/await 的机制,这其实也是一种巧妙的小技巧。
await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的 resolve 函数参数将作为 await 表达式的值,并继续执行 async function。
如果当前的并发数已经超过最大的并发数目,可以创建一个新的 Promise,并进行 await 操作,等待其他请求完成时调用 resolve,从而移除等待状态。因此,需要新增两个状态:当前的并发数目,以及用来存储 resolve 回调函数的数组。
class Concurrent {
private maxConcurrent: number = 2;
private list: Function[] = [];
private currentCount: number = 0;
constructor(count: number = 2) {
this.maxConcurrent = count;
}
public async add(fn: Function) {
this.currentCount += 1;
// 如果最大已经超过最大并发数
if (this.currentCount > this.maxConcurrent) {
// wait 是一个 Promise,只要调用 resolve 就会变成 fulfilled 状态
const wait = new Promise((resolve) => {
this.list.push(resolve);
});
// 在没有调用 resolve 的时候,这里会一直阻塞
await wait;
}
// 执行函数
await fn();
this.currentCount -= 1;
if (this.list.length) {
// 把 resolve 拿出来,调用,这样 wait 就完成了,可以往下面执行了
const resolveHandler = this.list.shift()!;
resolveHandler();
}
}
}
const run = async () => {
const concurrent = new Concurrent();
startTime = Date.now();
concurrent.add(timeout1);
concurrent.add(timeout2);
concurrent.add(timeout3);
concurrent.add(timeout4);
concurrent.add(timeout5);
};
run();
At 300 return 2
At 700 return 3
At 1000 return 1
At 1200 return 5
At 1200 return 4
总结
这两种方式都可以实现并发控制,只是实现机制不同,主要都依赖于 Promise。另外,上述实现中未考虑异常处理情况,开发者可根据需要自行添加错误处理逻辑。
到此这篇关于JavaScript/TypeScript 实现并发请求控制的示例代码的文章就介绍到这了,更多相关JavaScript 并发请求控制内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!