在项目中,我们经常会遇到同时发送多个HTTP请求的情况,对于同域名的请求,浏览器会限制同时发起的请求数量,当超过浏览器并发请求限制时,超过的请求将会等待,直到有请求返回时才会进行下一次请求。这是浏览器的一种保护机制,为了防止通过浏览器发起过量的请求,耗尽资源。在小程序中也会有同样的机制,例如,微信小程序中说明 wx.request 的最大并发限制是 10 个,wx.connectSocket 的最大并发限制是 5 个(参见 微信小程序官方文档-基础能力/网络/使用说明-网络-3.网络请求-使用限制 ),但是这里有个BUG,在小程序中如果并发请求超过10个,并不会等待而是直接失败,所以实际上需要用户自己处理并发请求数,在超过10个时,请求需要在队列中等待而不是直接失败。

假如我们的请求方法 request 形如:

1
2
3
4
5
6
7
8
9
function request(options) {
return new Promise((resolve, reject) => {
console.log(`${options} is start`)
setTimeout(() => {
console.log(`${options} is finished`)
resolve({ data: options });
}, 3000);
});
}

request 是一个通用的请求方法,它接受请求的参数,返回请求的 Promise,其中 options 是形如 {url: '', method: 'GET', params: {}} 的请求相关参数对象。

在业务中,我们会通过 fetch 方法获取请求内容并处理请求结果:

1
2
3
4
5
6
7
function fetch(options) => {
request(options).then((res) => {
// do something
}).catch((err) => {
// do something
});
}

fetch 是一个和业务相关的请求方法,它接受请求的参数,对请求的结果进行处理。

当发起多个并行请求时,最简单直接的方法就是使用 Promise.all:

1
2
3
4
5
// 多请求并行执行
function parallelRequest(optionsList) {
return Promise.all(optionsList.map(options => request(options)));
}
parallelRequest([1,2,3]).then(console.log);

如果需要请求串行发送,可以通过 async/await 的方式等待前一个请求返回后再发起下一个请求:

1
2
3
4
5
6
7
8
9
10
// 多请求串行执行
async function serialRequest(optionsList) {
let res = [];
for (let i = 0; i < optionsList.length; i++) {
let result = await request(optionsList[i]);
res.push(result);
}
return Promise.resolve(res);
}
serialRequest([1,2,3]).then(console.log);

串行是可以避免超过10个请求后,再次发送请求失败的问题,但是没有充分利用并行发送的数量,更好的方式是通过参数限制并发请求数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 限制并发数量的多请求执行
function multiRequest(optionsList = [], maxNum) {
const len = optionsList.length;
const result = new Array(len).fill(false);
let count = 0;
return new Promise((resolve, reject) => {
while (count < maxNum) {
next();
}
function next() {
let current = count++;
if (current >= len) {
!result.includes(false) && resolve(result);
return;
}
const options = optionsList[current];
request(options)
.then((res) => {
result[current] = res;
if (current < len) {
next();
}
}).catch((err) => {
result[current] = err;
if (current < len) {
next();
}
});
}
});
}
multiRequest([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3).then(console.log);

实际业务场景中,我们并不会在一个地方同时发起多个请求,而是在不同的地方发起请求,所以限制并发数量的的 request 方法应该是一个工具函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 提供给第三方使用的 request 方法,内部实现支持限制并发数量的请求
class LimitRequest {
constructor(limit) {
this.limit = limit; // 并发请求限制数量
this.tasks = []; // 请求等待队列
this.current = 0; // 当前并发请求数量
}
addRequest(reqFn) {
if (!reqFn || !(reqFn instanceof Function)) {
console.error('当前请求不是一个Function');
return;
}
this.tasks.push(reqFn);
if (this.current < this.limit) {
this.run();
}
}
async run() {
try {
this.current++;
const fn = this.tasks.shift();
await fn();
} catch(err) {
throw new Error(err);
} finally {
this.current--;
if (this.tasks.length > 0) {
this.run();
}
}
}
}
let limitRequest = new LimitRequest(3);
for (let i = 1; i <= 10; i++) {
limitRequest.addRequest(fetch.bind(null, i));
}

注意上面用的是 fetch 方法,当需要发送请求时,需要确定请求返回后的处理逻辑,将整体的方法写成 fetch 方法,并通过 addRequest 添加到并行的队列中去。

我们也可以优化一下,使用 request 方法,这样可以在请求时不必确定返回后的逻辑,将 请求 这个动作与业务分离:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class LimitRequest {
constructor(limit) {
this.limit = limit; // 并发限制数量
this.tasks = []; // 等待的请求队列
this.current = 0; // 当前进行中的请求数量
}
request(options) {
return new Promise((resolve, reject) => {
let reqFn = request.bind(null, options);
this.tasks.push([reqFn, resolve, reject]);
if (this.current < this.limit) {
this.run();
}
});
}
async run() {
try {
this.current++;
const [fn, resolve, reject] = this.tasks.shift();
await fn().then(res => {
resolve(res);
}).catch(err => {
reject(err);
});
} catch(err) {
throw new Error(err);
} finally {
this.current--;
if (this.tasks.length > 0) {
this.run();
}
}
}
}

let limitRequest = new LimitRequest(3);
let newRequest = limitRequest.request.bind(limitRequest);
for (let i = 1; i <= 10; i++) {
newRequest(i).then(console.log);
}

作为 utils 方法,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let limit = 3; // 并发限制数量
let tasks = []; // 等待的请求队列
let current = 0; // 当前进行中的请求数量
export function limitRequest(options) {
return new Promise((resolve, reject) => {
let reqFn = request.bind(null, options);
tasks.push([reqFn, resolve, reject]);
if (current < limit) {
run();
}
});
}
export function setLimit(num) {
limit = num;
}
async function run() {
try {
current++;
const [fn, resolve, reject] = this.tasks.shift();
await fn().then(res => {
resolve(res);
}).catch(err => {
reject(err);
});
} catch(err) {
throw new Error(err);
} finally {
current--;
if (tasks.length > 0) {
run();
}
}
}

for (let i = 1; i <= 10; i++) {
limitRequest(i).then(console.log);
}