本文正在参加「金石计划 . 瓜分6万现金大奖」
前言
如果请求数量很大,你该怎么处理?限制并发数量的具体操作代码是怎么样的?
来看看这个周下载量六十万的包,核心文件只有25行~
你能学到什么
- 异步请求的并发限制处理
- 生成器函数的妙用
- 你是否有使用过生成器函数?就是
function后面带个*那种。
- 你是否有使用过生成器函数?就是
- 你可能没用过甚至不了解的ES9 的语法知识
for await是什么语法,你有用过吗?
使用
const timeout = ms => new Promise(resolve => setTimeout(() => resolve(ms), ms));
for await (const ms of asyncPool(2, [1000, 5000, 3000, 2000], timeout)) {
console.log(ms);
}
从用法中可以看出 asyncPool接受三个参数
- 限制请求并发数
- 一个数组 —— 可迭代的对象,里面装着要处理的数据、内容
- 处理方法 —— 通常和 Promise 有关
注意 for 后面跟着的是 await
这意味着 asyncPool方法返回的是异步可迭代对象 —— 该方法是一个生成器方法
当值是以异步的形式出现时,例如在 setTimeout 或者另一种延迟之后,就需要异步迭代。
接下来看函数内部 —— 源代码位置在这 —— 删去注释才 22 行!
async function* asyncPool(concurrency, iterable, iteratorFn) {
const executing = new Set();
async function consume() {
const [promise, value] = await Promise.race(executing);
executing.delete(promise);
return value;
}
for (const item of iterable) {
// Wrap iteratorFn() in an async fn to ensure we get a promise.
// Then expose such promise, so it's possible to later reference and
// remove it from the executing pool.
const promise = (async () => await iteratorFn(item, iterable))().then(
value => [promise, value]
);
executing.add(promise);
if (executing.size >= concurrency) {
yield await consume();
}
}
while (executing.size) {
yield await consume();
}
}
module.exports = asyncPool;
接下来让我们一行行解读消化一下
声明
async function* asyncPool(concurrency, iterable, iteratorFn)
首先这是一个生成器函数
常规函数只会返回一个单一值(或者不返回任何值)。
而 生成器函数可以按需一个接一个地返回(“yield”)多个值。它们可与 iterable 完美配合使用,从而可以轻松地创建数据流。
executing 执行池子
const executing = new Set();
用来记录哪些任务正在执行,方便控制并发数量、获得是否完成全部任务的标记
注意这里放入的任务都是 Promise —— 后面会说到
consume "消费"方法
async function consume() {
const [promise, value] = await Promise.race(executing);
executing.delete(promise);
return value;
}
一个异步的方法,是处理任务的核心方法
Promise.race
const [promise, value] = await Promise.race(executing);
并行执行多个 promise但只等待第一个 settled 的 promise 并获取其结果(或 error)
也就是将执行池中的任务都执行,获取其中第一个完成的任务
注意这里返回的形式 [promise, value]
- 第一项是 promise —— 一个自身的引用(看到后面才知道,这里你看不出来正常,我就先说一下)
- 第二项就是真正有用的返回值
删除标记
executing.delete(promise);
利用刚才返回的引用,从执行池中删除
核心循环
for (const item of iterable) {
const promise = (async () => await iteratorFn(item, iterable))().then(
value => [promise, value]
);
executing.add(promise);
if (executing.size >= concurrency) {
yield await consume();
}
}
Promise 包装
const promise = (async () => await iteratorFn(item, iterable))().then(
value => [promise, value]
);
调用处理方法,并通过(async () => await ...)()包裹,来确保是一个 Promise —— 不好保证使用者传入的 iteratorFn是 Promise嘛,这样包一层就肯定是了
然后.then(value => [promise, value]) 将返回值包装成 [自身引用, 结果]
添加标记
executing.add(promise);
将该任务的自身引用加入 执行池中
这个自身引用的作用上面也看到了,就是用来从执行池的添加、删除的
执行下一个任务
if (executing.size >= concurrency) {
yield await consume();
}
如果执行池中任务数量大于等于并发限制了,就等待并发池中有一个任务执行结束之后再继续
收尾
while (executing.size) {
yield await consume();
}
跳出核心循环之后,就说明所有任务不是结束了就是再执行池中
我们处理一下执行池中剩余的任务就行了
总览
async function* asyncPool(concurrency, iterable, iteratorFn) {
const executing = new Set(); // 记录哪些任务正在执行,方便控制并发数量、获得是否完成全部任务的标记
async function consume() {
// 将执行池中的任务都执行,获取其中第一个完成的任务
const [promise, value] = await Promise.race(executing);
executing.delete(promise); // 利用刚才返回的引用,从执行池中删除
return value;
}
for (const item of iterable) {
//通过(async () => await ...)()包裹,来确保是一个 Promise
const promise = (async () => await iteratorFn(item, iterable))().then(
value => [promise, value] // 将返回值包装成 [自身引用, 结果]
);
executing.add(promise); //将该任务的自身引用加入 执行池中
// 如果执行池中任务数量大于等于并发限制了,就等待并发池中有一个任务执行结束之后再继续
if (executing.size >= concurrency) {
yield await consume();
}
}
// 处理一下执行池中剩余的任务
while (executing.size) {
yield await consume();
}
}
module.exports = asyncPool;
相关阅读
文中措辞、知识点、格式如有疑问或建议,欢迎评论~你对我很重要~
🌊如果有所帮助,欢迎点赞关注,一起进步⛵这对我很重要~