并发请求封装
刀刀
4/7/2025
0 字
0 分钟
需求
js
function fn1 (params) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1, params)
reject(1)
}, 1000)
})
}
function fn2 (params) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(2, params)
resolve(2)
}, 1000)
})
}
function fn3 (params) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(3, params)
resolve(3)
}, 1000)
})
}
有一个需求,用于一次发任意多个请求,当请求失败时则重新发送,直到所有的请求都成功发送,或超出最大重试次数,才返回最终结果。
js
let res = {
// success response
success: [],
// error response
error: [],
}
进阶条件是把该方法封装为可复用的并发请求工具。
思路梳理
- 要并发的请求携程方法,传给并发函数工具
- 给传入的方法生成内部
id
,方便重发 - 给请求返回的
Promise
包装一层,附带id
- 利用
Promise.allSettled
发送请求 - 循环返回结果,找出失败的
id
,找到原方法再次调用发请求
实操
创建类
写一个类,创建 id
并在映射表中把 id
和方法映射,再给函数方法属性赋值一个 id
。后续失败时可拿到该 id
,通过该映射表获取对应失败方法重试。
js
class RequestList {
map = {}
list = []
successlist = []
errorlist = []
/*
fn: 需要发送请求的数组
reTryTime:重试次数,默认为3次
*/
constructor(fnList, reTryTime = 3) {
fnList.forEach((fn, index) => {
let _id = 'id' + index
// 用id和方法,映射进map
this.map[_id] = fn
// 把id给到方法的静态属性
fn.id = _id
// 重发次数统计
fn.reTry = reTryTime
fn.hasTry = 0
})
}
}
new RequestList([fn1.bind(this, {a: 1, b: 2}), fn2, fn3])
现在准备发送请求,有以下几个请求发送方式可选择:
- 循环请求数组
Promise.all
Promise.allSelected
下面来讲讲为什么选择 Promise.allSelected
。如果选择循环的方式,则方法都是各自调用,需要自己写方法判断是否全部请求都发送,结果是成功还是失败。
如果选择 Promise.all
方式,则会出现一个失败,后续都不发送的情况。而在这个需求中,则是每个请求都要发送,记录各自的成功失败情况。因此 Promise.all
方式不适用。
js
Promise.all([fn1.bind(this, {a: 1, b: 2})(), fn2(), fn3()])
.then(res => {
console.log(res) // 无打印,因为fn1返回的是失败reject,后续方法不在请求
})
.catch(err => {
console.log(err)
})
使用 Promise.allSelected
方式,无论其他方法是否报错,每个方法都会调用。最终返回该方法的请求状态。
js
Promise.allSelected([fn1.bind(this, {a: 1, b: 2})(), fn2(), fn3()])
.then(res => {
console.log(res)
})
发请求与失败重发
js
class RequestList {
// ...
send() {
let _que = []
this.list.forEach(fn => {
_que.push(fn())
})
// 后续失败需要重新调用,因此写在函数内,方便重发
function sendAllSelected() {
Promise.allSelected(_que).then(resList => {
console.log(resList)
})
}
}
}
new RequestList([fn1.bind(this, {a: 1, b: 2}), fn2, fn3]).send()
查看打印结果:
可以发现虽然已经看得出来第一条请求失败了,但是没有对应的 id
后续无法得知是哪条数据失败需要重新发送。因此还需要包裹一层 id
。
js
class RequestList {
// ...
createPromise(fn) {
// 返回一个新的 promise,返回对应的id和成功或失败的信息
return new Promise((resolve, reject) => {
fn().then(res => {
resolve({
id: fn.id,
value: res
})
}).catch(err => {
reject({
id: fn.id,
value: err
})
})
})
}
send() {
let _que = []
this.list.forEach(fn => {
// 不把原来的fn放进去,而是把包裹的新函数放进去
_que.push(this.createPromise(fn))
})
// 后续失败需要重新调用,因此写在函数内,方便重发。这里不能通过 function 声明函数,不然里面的 this 指向 undefined
const sendAllSelected = () => {
Promise.allSelected(_que).then(resList => {
_que = []
resList.forEach(singres => {
if(singres.status === 'fulfilled') {}
// 失败,找出对应id,然后找到原方法
else {
let _id = singres.reason.id
let _fn = this.map[_id]
_que.push(this.createPromise(_fn)) // 出错了,加入带请求队列中再次请求
_fn.reTry += 1
}
})
if (_que.length === 0) {}
else {
sendAllSelected()
}
})
}
sendAllSelected()
}
}
new RequestList([fn1.bind(this, {a: 1, b: 2}), fn2, fn3]).send()
次数限制与结果添加
添加次数限制功能,通过重发次数变量和总发送次数变量判断对比,如果没达到次数则往请求数组内添加函数重新发送;如果次数达到限制,则添加到失败队列数组内。
最后在请求队列数组为空时返回 Promise
,返回成功队列和失败队列。
js
class RequestList {
// ...
send() {
return new Promise((resolve) => {
let _que = []
this.list.forEach(fn => {
// 不把原来的fn放进去,而是把包裹的新函数放进去
_que.push(this.createPromise(fn))
})
// 后续失败需要重新调用,因此写在函数内,方便重发。这里不能通过 function 声明函数,不然里面的 this 指向 undefined
const sendAllSelected = () => {
Promise.allSelected(_que).then(resList => {
_que = [] // 请求发送后,先置空
resList.forEach(singres => {
// 成功,放到成功队列
if(singres.status === 'fulfilled') {
this.successlist.push(singres.value.value) // [!code foucs]
}
// 失败,找出对应id,然后找到原方法
else {
let _id = singres.reason.id
let _fn = this.map[_id]
// 判断是否超出最大次数
if(_fn.hasTry < _fn.reTry) { // [!code foucs]
_que.push(this.createPromise(_fn)) // 出错了,加入带请求队列中再次请求
_fn.reTry += 1
} // [!code foucs]
else { // [!code foucs]
// 超出最大失败次数,放到失败队列 // [!code foucs]
this.errorList.push(singres.reason.value) // [!code foucs]
} // [!code foucs]
}
})
if (_que.length === 0) {
// 没有需要发送的请求了,返回成功队列和失败队列 // [!code foucs]
resolve({ // [!code foucs]
success: this.successList, // [!code foucs]
error: this.errorList, // [!code foucs]
}) // [!code foucs]
}
else {
sendAllSelected()
}
})
}
sendAllSelected()
})
}
}
new RequestList([fn1.bind(this, {a: 1, b: 2}), fn2, fn3]).send().then(res => {
console.log(res)
})
总体代码
查看代码
js
class RequestList {
map = {}
list = []
successlist = []
errorlist = []
constructor(fnList) {
fnList.forEach((fn, index) => {
let _id = 'id' + index
// 用id和方法,映射进map
this.map[_id] = fn
// 把id给到方法的静态属性
fn.id = _id
// 重发次数统计
fn.reTry = reTryTime
fn.hasTry = 0
})
}
createPromise(fn) {
// 返回一个新的 promise,返回对应的id和成功或失败的信息
return new Promise((resolve, reject) => {
fn().then(res => {
resolve({
id: fn.id,
value: res
})
}).catch(err => {
reject({
id: fn.id,
value: err
})
})
})
}
send() {
return new Promise((resolve) => {
let _que = []
this.list.forEach(fn => {
// 不把原来的fn放进去,而是把包裹的新函数放进去
_que.push(this.createPromise(fn))
})
// 后续失败需要重新调用,因此写在函数内,方便重发。这里不能通过 function 声明函数,里面的 this 指向 undefined
const sendAllSelected = () => {
Promise.allSelected(_que).then(resList => {
_que = [] // 请求发送后,先置空
resList.forEach(singres => {
// 成功,放到成功队列
if(singres.status === 'fulfilled') {
this.successlist.push(singres.value.value)
}
// 失败,找出对应id,然后找到原方法
else {
let _id = singres.reason.id
let _fn = this.map[_id]
// 判断是否超出最大次数
if(_fn.hasTry < _fn.reTry) {
_que.push(this.createPromise(_fn)) // 出错了,加入带请求队列中再次请求
_fn.reTry += 1
}
else {
// 超出最大失败次数,放到失败队列
this.errorList.push(singres.reason.value)
}
}
})
if (_que.length === 0) {
// 没有需要发送的请求了,返回成功队列和失败队列
resolve({
success: this.successList,
error: this.errorList,
})
}
else {
sendAllSelected()
}
})
}
sendAllSelected()
})
}
}
new RequestList([fn1.bind(this, {a: 1, b: 2}), fn2, fn3]).send().then(res => {
console.log(res)
})