JavaScript实现并发请求

2023-12-28 16:39:33

并发即在同一时间段内执行多个任务或者处理多个任务,这有什么用呢?比如在前端一些大文件的分片上传,如果分片后上传完成一个之后再上传,那么效率就会比较低,但是如果不限制,一次性都发送,那么又会太大,所以我们需要控制一下发送的数量,也就是在一次发送的队列中,最多允许多少个任务一起执行

思路分析

  1. 任务定义:这个任务的结果只会成功,无论这个任务的结果是成功还是失败,都算做是成功的状态
  2. 我们假设存在20个任务,并发数量为5,如果最大数量为5,是不是表示在第一次发送请求的时候,我们需要发送5个这样的任务
  3. 需要任务的话,那么首先我们就需要有这个任务,也就是说我们要封装一个异步请求函数,而每次并发执行的任务其实就是调用一次这个函数
  4. 那当最开始发送的5个任务中当其中某一个完成之后,又应该进行怎么样的操作呢?比如第二个任务完成了,其他四个任务还在进行中,此时任务队列的任务数量为4,那么我们就要在这个任务执行完成的时候去调用下一个任务
  5. 那怎么调用下一个任务呢?上传的是分片的文件数据,那么就会有一个存储这个分片数据的数组吧,数组怎么取值呢?是不是通过索引就可以呢?所以当第二个任务完成的时候,只需要通过 arr[i] 就可以获取下一个分片的数据了,并利用我们封装好的函数将其包装为一个任务,进行发送
  6. 所以为了得到这个 i 的值,那么每当封装的任务进入发送队列之后,就需要将索引+1,从而取下一个任务,而有了这个执行逻辑之后,我们就可以实现一个维持固定数量执行的任务队列了
  7. 比如最开始的发送了5个任务,也就意味着索引自增了5次,那么第二个任务完成了,又因为索引的自增,就可以顺序的取到下一个任务,将这个任务进入到一开始第二个的任务队列位置进行发送,在这个基础上不停的循环,从而实现并发请求

具体实现

  1. 准备数据

    const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
    
  2. 这里就简单的用一些数字了,封装一个函数,将数据包裹为一个异步请求任务

    function uploadData(value) {
    	return new Promise((resolve, reject) => {
    		// 模拟请求
    		setTimeout(() => {
    			resolve(`数据【${value}】执行完成`)
    		}, 1000)
    	})
    }
    
  3. 现在就要开发编写真正实现并发请求的方法了,这个方法,根据分析,应该接收两个参数,数据和最大数量,返回是一个Promise对象,当所有请求完成之后执行成功,如下:

    function concurrentRequests(list, maxNum) {
    	return new Promise(resolve => {})
    }
    
  4. 再次基础上,我们还需要什么,是不是需要定义数组来接收每个任务执行完成后的结果呢,成功和失败都放入进来

    function concurrentRequests(list, maxNum) {
      const result = []
      
      return new Promise(resolve => {
        
      })
    }
    
  5. 在这个函数内部,我们还需要一个函数来帮助我们取数据,并调用 uploadData 发送任务,并或结果存入 result 数组

    function concurrentRequests(list, maxNum) {
    	const result = []
    	let index = 0
    
    	return new Promise(resolve => {
    		// 定义函数
    		async function request() {
    			// 保存索引-可以保证存储的结果和原始数据数组的顺序一致
    			const i = index
    			// 调用一次之后索引自增
    			index++
    			try {
    				// 接受成功的结果
    				const resp = await uploadData(list[i])
    				// 利用保存的索引存入结果数组
    				result[i] = resp
    			} catch (error) {
    				// 失败的结果也保存
    				request[i] = error
    			} finally {
    				// 无论成功或失败,在在此处执行,重新调用 request 方法
    				//  - 同时保证不会取到空的数据发送
    				if (index < list.length) {
    					request()
    				}
    			}
    		}
    	})
    }
    
  6. 现在就应该根据最开始的 maxNum 来决定发送时的数量

    function concurrentRequests(list, maxNum) {
    	const result = []
    	let index = 0
    
    	return new Promise(resolve => {
    		async function request() {
    			const i = index
    			index++
    			try {
    				const resp = await uploadData(list[i])
    				result[i] = resp
    			} catch (error) {
    				request[i] = error
    			} finally {
    				if (index < list.length) {
    					request()
    				}
    			}
    		}
            
            // 使用循环来达到队列发送任务数量
    		for (let i = 0; i < maxNum; i++) {
    			request()
    		}
    	})
    }
    
  7. 为什么使用循环可以呢,这样不是还是一次一次的发送吗,这是因为在循环中是同步的,而任务是异步的,所以就说只有当同步任务执行完成之后才会执行异步

  8. 那什么时候才算成功呢?是直接判断当索引等于数组长度-1吗?那肯定不行,当最后一个任务完成的时候,此时索引已经满足数组长度-1了,但是你无法保证和他一个在一个队列的任务已经完成了,所以我们还要定义一个变量,来确定是否完成

    const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
    
    function uploadData(value) {
    	return new Promise((resolve, reject) => {
    		// 模拟请求
    		setTimeout(() => {
    			resolve(`数据【${value}】执行完成`)
    			console.log(`上传数据【${value}】完成`)
    		}, getRandomInt(1, 3) * 1000)
    	})
    }
    
    // 生成随机数
    function getRandomInt(min, max) {
    	return Math.floor(Math.random() * (max - min + 1)) + min
    }
    
    function concurrentRequests(list, maxNum) {
    	const result = [] // 结果数组
    	let index = 0 // 索引
    	let count = 0 // 完成的任务计数
    
    	return new Promise(resolve => {
    		async function request() {
    			const i = index
    			index++
    			try {
    				const resp = await uploadData(list[i])
    				result[i] = resp
    			} catch (error) {
    				request[i] = error
    			} finally {
    				// 计数+1
    				count++
    				if (count === list.length) {
    					resolve(result)
    				}
    
    				if (index < list.length) {
    					request()
    				}
    			}
    		}
    		for (let i = 0; i < maxNum; i++) {
    			request()
    		}
    	})
    }
    
    concurrentRequests(data, 5).then(res => {
    	console.log(res)
    })
    
  9. 剩下的就是一些细节的判断了

    const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
    
    function uploadData(value) {
    	return new Promise((resolve, reject) => {
    		// 模拟请求
    		setTimeout(() => {
    			resolve(`数据【${value}】执行完成`)
    			console.log(`上传数据【${value}】完成`)
    		}, getRandomInt(1, 3) * 1000)
    	})
    }
    
    // 生成随机数
    function getRandomInt(min, max) {
    	return Math.floor(Math.random() * (max - min + 1)) + min
    }
    
    function concurrentRequests(list, maxNum) {
    	const result = [] // 结果数组
    	let index = 0 // 索引
    	let count = 0 // 完成的任务计数
    
    	return new Promise(resolve => {
        async function request() {
          if (!list.length) { 
            // 数组没有数组时返回空数组
            resolve([])
          }
    
    			// 保存索引-可以保证存储的结果和原始数据数组的顺序一致
    			const i = index
    			// 调用一次之后索引自增
    			index++
    			try {
    				// 接受成功的结果
    				const resp = await uploadData(list[i])
    				// 利用保存的索引存入结果数组
    				result[i] = resp
    			} catch (error) {
    				// 失败的结果也保存
    				request[i] = error
    			} finally {
    				// 计数+1
    				count++
    				if (count === list.length) {
    					resolve(result)
    				}
    
    				// 无论成功或失败,在在此处执行,重新调用 request 方法
    				//  - 同时保证不会取到空的数据发送
    				if (index < list.length) {
    					request()
    				}
    			}
    		}
    
    		// 当数组长度小于并发数时
    		for (let i = 0; i < Math.min(list.length, maxNum); i++) {
    			request()
    		}
    	})
    }
    
    concurrentRequests(data, 5).then(res => {
    	console.log(res)
    })
    
  10. 现在增加一些输出语句,查看结果

    function uploadData(value) {
    	console.log(`start:数据【${value}】...`)
    	return new Promise((resolve, reject) => {
    		// 模拟请求
    		setTimeout(() => {
    			resolve(`数据【${value}】执行完成`)
    			console.log(`end:数据【${value}`)
    		}, getRandomInt(1, 3) * 1000)
    	})
    }
    
    concurrentRequests(data, 5).then(res => {
    	console.log(res)
    })
    
  11. 结果如图:

    在这里插入图片描述

  12. 可以看到,因为随机的原因,导致执行完成的顺序虽然不一致,但是还是保证了结果与数据源的顺序一致

源码展示

const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]

// 模拟上传数据方法
function uploadData(value) {
	console.log(`start:数据【${value}】...`)
	return new Promise((resolve, reject) => {
		// 模拟请求
		setTimeout(() => {
			resolve(`数据【${value}】执行完成`)
			console.log(`end:数据【${value}`)
		}, getRandomInt(1, 3) * 1000)
	})
}

// 生成随机数
function getRandomInt(min, max) {
	return Math.floor(Math.random() * (max - min + 1)) + min
}

function concurrentRequests(list, maxNum) {
	const result = [] // 结果数组
	let index = 0 // 索引
	let count = 0 // 完成的任务计数

	return new Promise(resolve => {
		async function request() {
			if (!list.length) {
				// 数组没有数组时返回空数组
				resolve([])
			}

			// 保存索引-可以保证存储的结果和原始数据数组的顺序一致
			const i = index
			// 调用一次之后索引自增
			index++
			try {
				// 接受成功的结果
				const resp = await uploadData(list[i])
				// 利用保存的索引存入结果数组
				result[i] = resp
			} catch (error) {
				// 失败的结果也保存
				request[i] = error
			} finally {
				// 计数+1
				count++
				if (count === list.length) {
					resolve(result)
				}

				// 无论成功或失败,在在此处执行,重新调用 request 方法
				//  - 同时保证不会取到空的数据发送
				if (index < list.length) {
					request()
				}
			}
		}

		// 当数组长度小于并发数时
		for (let i = 0; i < Math.min(list.length, maxNum); i++) {
			request()
		}
	})
}

concurrentRequests(data, 5).then(res => {
	console.log(res)
})

文章来源:https://blog.csdn.net/qq_53109172/article/details/135242983
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。