前端并发请求

正文

在前端开发中, 并发请求能够有效的提高用户体验, 尤其是需要预览大量图片的项目, 一次请求多张图片能大大的优化加载速度. 比如淘宝的商品列表等

这里就一步一步的写出一个并发请求方法, 由简入繁的完善功能

第一步: 先写一个基础的循环发送请求函数, 这里我使用 fetch 发送请求, 接收 url 和 options. 使用 while 循环发送请求

1. 定义的请求数组, 注意我的第二个请求用 POST, 可以验证 options 是否生效

```

const urls = [
  {
    url: "/api/hello?tn=75144485_5_dg&ch=2&ie=utf-8&wd=1",
    options: { method: "GET" },
  },
  {
    url: "/api/hello?tn=75144485_5_dg&ch=2&ie=utf-8&wd=2",
    options: { method: "POST" },
  },
  {
    url: "/api/hello?tn=75144485_5_dg&ch=2&ie=utf-8&wd=3",
    options: { method: "GET" },
  },
  {
    url: "/api/hello?tn=75144485_5_dg&ch=2&ie=utf-8&wd=4",
    options: { method: "GET" },
  },
  {
    url: "/api/hello?tn=75144485_5_dg&ch=2&ie=utf-8&wd=5",
    options: { method: "GET" },
  },
];
```

2. 定义 targetIndex 变量, 用于记录发送到哪一个请求了. 定义 results 数组, 用于保存请求结果. 使用 while 循环请求全部数据, 直到 targetIndex > urls.length 为止

	1. 注意: 虽然 while 是同步代码, 但因为在循环内部使用了 await, 它会暂停每一轮的执行, 变成了同步顺序的等待异步操作完成. 简单说就是等待请求完成后, 才会进入下一个循环, 所以此时可以放心的用 targetIndex 当作 results 的索引

```
const concurrentRequests = async (urls: Urls[], max: number) => {
  let targetIndex = 0;
  const results = [];
  while (targetIndex < urls.length) {
    const { url, options } = urls[targetIndex]!;
    const res = await fetch(url, options);
    results[targetIndex] = res;
  }
};


​ 模拟请求延迟的接口
​ export default defineEventHandler(async (event) => {
​ const query = getQuery(event);
​ const delay = Number(query.delay || 1000);
​ const name = query.name || ‘unknown’;

// 模拟延迟
await new Promise((resolve) => setTimeout(resolve, delay));

    return {
        message: `Hello ${name}`,
        timestamp: Date.now()
    };
})
```
  1. 此时一个普通的循环请求函数就完成了, 会把传入 urls 数组里的请求全部发送, 但是需要等待上一个请求完成才能发送下一个请求, 一旦某个请求阻塞, 就会拉长整体请求时间

第二步: 完善并发请求功能

  1. 使用 Promise.all() API, 实现并发请求

    1. Promise.all(): 接收可迭代的对象, 比如数组或字符串
  2. 使用 async 隐式的返回一个 Promise

    1. 异步函数总是返回一个 promise。如果一个异步函数的返回值看起来不是 promise,那么它将会被隐式地包装在一个 promise 中

      1. async 修饰后的函数
      async function foo() {
        return 1;
      }
      
      类似于
      
      function foo() {
        return Promise.resolve(1);
      }
      
      2. await 等待返回值
      async function foo() {
        await 1;
      }
      
      等价于
      
      function foo() {
        return Promise.resolve(1).then(() => undefined);
      }
      
  3. 利用数组的 map() 方法和 async 隐式返回 Promise 的特性, 拿到由 Promise 组成的数组, 传递给 Promise.all()

    1. 使用 Array(max) 的方式创建一个有长度但没有元素值得空洞数组, 它不能被 map forEach filter 等迭代方法使用; 使用 .fill(null) 填充该空洞数组

      const _request = Array(max)
      .fill(null)
      .map(async () => {
          while (targetIndex < urls.length) {
              const { url, options } = urls[targetIndex]!;
              const res = await fetch(url, options);
              results[targetIndex] = res;
          }
      });
      console.log(_request,"@_request")  ===> [Promise, Promise, Promise]
      await Promise.all(_request);
      
  4. 看上去似乎大功告成, 但此时代码还不能正确运行, 因为我们并没有在循环里改变 targetIndex 的值, 会一直发送请求

    1. 那么我们在循环里添加 targetIndex++ 就可以了吗? 并不是, 这会导致多个并发任务出现错乱. await fetch() 是异步的, 当两个并发执行时, 第一个请求还没结束, 第二个请求就修改了 targetIndex 的值, 索引值错乱导致 results 的映射关系也是错的

    2. 所以我们需要在每个循环中锁住当前要处理的索引, 通过 const currentIndex = targetIndex; 的方式保存正确的索引值

      const _request = Array(max)
      .fill(null)
      .map(async () => {
        while (targetIndex < urls.length) {
          const currentIndex = targetIndex;
          targetIndex++;
          const { url, options } = urls[currentIndex]!;
          const res = await fetch(url, options);
          results[currentIndex] = res;
        }
      });
      await Promise.all(_request);
      return results;
      
    3. 此时大功告成, 然后就是添加各种边界判断及细节, 比如用户传入空数组; 传入的不是数组; 最大并发数传入 0; 最大并发请求数超过数组长度等

  5. 此时有些同学还不明白上述代码是怎么运行的, 下面我详细解释一下

    1. 当正确的调用方法如 concurrentRequests(urls, 3) 后,
      1. .map 会返回三个 Promise 组成的数组, 此时 map 循环结束.
      2. 这三个 Promise 是三个异步任务(worker),每个包含一个 while 循环
      3. 每个 while 循环会不断尝试从共享的 targetIndex 中领取下一个任务,执行 await fetch()
      4. 因为使用了 await,所以每个任务在发送请求后会暂停,等待响应返回后再继续下一次循环
      5. 当三个 worker 同时开始并发请求的这一轮,就可以称为“第一波并发请求”
    2. 第一波并发请求结束后,
      1. 每个 worker 在前一次 fetch 完成后,重新进入 while 判断是否还有任务(即:targetIndex < urls.length
      2. 如果还有,就继续领取新的任务,执行下一个 fetch,这就组成了“第二波并发请求”
      3. 所谓“第二波”,其实不是统一触发的,而是每个 worker 自己节奏地进入下一轮
    3. 这个过程会一直持续到 targetIndex 大于或等于 urls.length 为止, 然后等待 Promise.all 拿到全部响应后, return 最终结果
完整代码及 Demo
/**
 * 并发请求方法
 */

// 定义请求配置接口
export interface RequestConfig {
    url: string;
    options?: RequestInit;
}
// 定义请求结果接口
export interface RequestResult<T> {
    status: 'fulfilled' | 'rejected';
    data?: T;
    error?: string;
}

export const utilsConcurrencyRequest = async <T>(urls: RequestConfig[], maxConcurrent: number = 3): Promise<RequestResult<T>[]> => {
    // 参数验证
    if (!Array.isArray(urls)) {
        throw new TypeError('urls参数必须是数组');
    }

    if (urls.length === 0) {
        return [];
    }

    if (maxConcurrent <= 0) {
        throw new RangeError('maxConcurrent必须大于0');
    }
    const results: RequestResult<T>[] = [];
    let targetIndex = 0;

    // 创建工作函数数组
    const workers = Array(Math.min(maxConcurrent, urls.length))
        .fill(null)
        .map(async () => {
            while (targetIndex < urls.length) {
                // 先赋值, 后++, 执行顺序为: currentIndex = targetIndex; targetIndex = targetIndex + 1
                const currentIndex = targetIndex++;
                const { url = '', options } = urls[currentIndex] || {}
                try {
                    const response = await fetch(url, options);
                    if (!response.ok) throw new Error(`HTTP ${response.status}`);
                    // response json 化
                    results[currentIndex] = { status: 'fulfilled', data: await response.json() };
                } catch (error) {
                    results[currentIndex] = {
                        status: 'rejected',
                        error: error instanceof Error ? error.message : String(error)
                    };
                }
            }
        });

    // 等待所有工作完成
    await Promise.all(workers);
    console.log(results, "@results")
    return results;
}
Promise 并发

Promise 类提供了四个静态方法来促进异步任务的:

  • Promise.all()

    所有传入的 Promise 都被兑现时兑现;在任意一个 Promise 被拒绝时拒绝。

  • Promise.allSettled()

    所有的 Promise 都被敲定时兑现。

  • Promise.any()

    任意一个 Promise 被兑现时兑现;仅在所有的 Promise 都被拒绝时才会拒绝。

  • Promise.race()

    任意一个 Promise 被敲定时敲定。换句话说,在任意一个 Promise 被兑现时兑现;在任意一个的 Promise 被拒绝时拒绝。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值