面试常考的前端请求并发控制怎么写?|tiny-async-pool 源码阅读

2,427 阅读4分钟

本文正在参加「金石计划 . 瓜分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接受三个参数

  1. 限制请求并发数
  2. 一个数组 —— 可迭代的对象,里面装着要处理的数据、内容
  3. 处理方法 —— 通常和 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]

  1. 第一项是 promise —— 一个自身的引用(看到后面才知道,这里你看不出来正常,我就先说一下)
  2. 第二项就是真正有用的返回值

删除标记

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;

相关阅读

文中措辞、知识点、格式如有疑问或建议,欢迎评论~你对我很重要~
🌊如果有所帮助,欢迎点赞关注,一起进步⛵这对我很重要~