正文
在前端开发中, 并发请求能够有效的提高用户体验, 尤其是需要预览大量图片的项目, 一次请求多张图片能大大的优化加载速度. 比如淘宝的商品列表等
这里就一步一步的写出一个并发请求方法, 由简入繁的完善功能
第一步: 先写一个基础的循环发送请求函数, 这里我使用 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()
};
})
```
- 此时一个普通的循环请求函数就完成了, 会把传入 urls 数组里的请求全部发送, 但是需要等待上一个请求完成才能发送下一个请求, 一旦某个请求阻塞, 就会拉长整体请求时间
第二步: 完善并发请求功能
-
使用 Promise.all() API, 实现并发请求
- Promise.all(): 接收可迭代的对象, 比如数组或字符串
-
使用 async 隐式的返回一个 Promise
-
异步函数总是返回一个 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); }
-
-
利用数组的 map() 方法和 async 隐式返回 Promise 的特性, 拿到由 Promise 组成的数组, 传递给 Promise.all()
-
使用 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);
-
-
看上去似乎大功告成, 但此时代码还不能正确运行, 因为我们并没有在循环里改变 targetIndex 的值, 会一直发送请求
-
那么我们在循环里添加 targetIndex++ 就可以了吗? 并不是, 这会导致多个并发任务出现错乱. await fetch() 是异步的, 当两个并发执行时, 第一个请求还没结束, 第二个请求就修改了 targetIndex 的值, 索引值错乱导致 results 的映射关系也是错的
-
所以我们需要在每个循环中锁住当前要处理的索引, 通过 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; -
此时大功告成, 然后就是添加各种边界判断及细节, 比如用户传入空数组; 传入的不是数组; 最大并发数传入 0; 最大并发请求数超过数组长度等
-
-
此时有些同学还不明白上述代码是怎么运行的, 下面我详细解释一下
- 当正确的调用方法如
concurrentRequests(urls, 3)后,- .map 会返回三个 Promise 组成的数组, 此时 map 循环结束.
- 这三个 Promise 是三个异步任务(worker),每个包含一个
while循环 - 每个
while循环会不断尝试从共享的targetIndex中领取下一个任务,执行await fetch() - 因为使用了
await,所以每个任务在发送请求后会暂停,等待响应返回后再继续下一次循环 - 当三个 worker 同时开始并发请求的这一轮,就可以称为“第一波并发请求”
- 第一波并发请求结束后,
- 每个 worker 在前一次
fetch完成后,重新进入while判断是否还有任务(即:targetIndex < urls.length) - 如果还有,就继续领取新的任务,执行下一个
fetch,这就组成了“第二波并发请求” - 所谓“第二波”,其实不是统一触发的,而是每个 worker 自己节奏地进入下一轮
- 每个 worker 在前一次
- 这个过程会一直持续到 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 被拒绝时拒绝。
890

被折叠的 条评论
为什么被折叠?



