跳转到内容

并发请求封装

刀刀

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: [],
}

进阶条件是把该方法封装为可复用的并发请求工具。

思路梳理

  1. 要并发的请求携程方法,传给并发函数工具
  2. 给传入的方法生成内部 id ,方便重发
  3. 给请求返回的 Promise 包装一层,附带 id
  4. 利用 Promise.allSettled 发送请求
  5. 循环返回结果,找出失败的 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])

现在准备发送请求,有以下几个请求发送方式可选择:

  1. 循环请求数组
  2. Promise.all
  3. 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)
})