前言
今天在公司写代码时遇到了一个场景:
通过在网页端上传发票文件到服务端,由服务端进行OCR识别。在原需求中只能进行单文件上传,后来需求更改,前端需要一次性上传多个发票文件。但是由于文件只能单个识别,所以后端不支持多个文件同时上传,因此我只能在前端选择多文件之后进行轮训,每一次请求发送一个文件,当一个文件识别完成并且返回结果之后再进行下一个文件上传。当我使用forEach进行遍历循环,并且给回调函数添加async,在回调中使用await接收返回的结果,但是报了错误。发现这些请求竟然是同时进行的,并没有因为await而进行等待。
于是我在网上搜索资料之后明白了原因,下面我将尽可能的通过简洁的语言来总结这个问题。
出现的问题
下面我将使用简单的示例来复现这个问题。
定义开发服务器
const express = require('express')
const cors = require('cors')
const app = express()
const port = 3000
app.use(cors());
app.use(express.json({ limit: '50mb' })); // 解析JSON请求体
app.use(express.urlencoded({ extended: false, limit: '50mb' })); // 解析URL编码的请求体
app.get('/getData', async (req, res) => {
try {
setTimeout(() => {
res.send([ 'vue', 'html', 'css', 'js' ])
}, 500)
} catch(err) {
res.send({
code: 1001,
data: null,
message: err
})
}
})
app.listen(port, () => {
console.log(`端口在${port}启动`);
})
上面的代码使用express定义了一个开发服务器(如不明白express的用法请自行了解),它描述了一个get请求接路由 /getDate,结果返回一个数组。
前端请求
// 访问接口函数
function getData () {
return fetch('http://localhost:3000/getData').then(res => {
return res.json()
}).then(res => {
return res
})
}
// 请求数据函数
async function initDataFn () {
let fileList = [1,2,3,4,5,6,7,8,9,10]
fileList.forEach(async () => {
let res = await getData()
console.log('map', res)
})
}
initDataFn()
在上面的代码中,首先定义了一个访问接口的函数,内部使用了fetch来进行请求,并且返回指定请求路径的数据。然后定义了一个请求数据的函数,在这里,我们将模拟进行10次请求,并且使用了async函数以及await进行接收返回的数据。
我的期望是每一次请求完成并且得到数据之后再进行下一个请求,但是在网络请求中是这样的:
从上图中可以看到,网络请求分为两段,因为chrome浏览器最大并发量是6个,所以前一段有6个请求同时进行,之后再进行后面的请求。
使用for循环
上面的方法不是一个请求成功返回之后再进行下一个请求,所以我改用了传统的for循环来请求,代码如下:
function getData () {
return fetch('http://localhost:3000/getData').then(res => {
return res.json()
}).then(res => {
return res
})
}
// 传统的for循环
async function initDataFn () {
for (let i = 0; i < 10; i++) {
let res = await getData()
console.log('res', res)
}
}
initDataFn()
网络请求的结果如下:
可以看到,网络请请求是一条直线,那就说明一次请求完毕之后再进行下一个请求,也就是我期望的效果。
原因
下面我将介绍这两种循环的区别。
forEach
当使用forEach循环时,并在其中调用了async函数,要理解为什么出现异步请求“同步”的问题,就要知道两点
- forEach是同步执行的,也就是说它会一次性的遍历完数组的所有元素。
- forEach并不等待async函数执行完毕之后再继续下一次迭代,即使回调函数内有async函数,forEach仍然会立即执行下一个迭代,而不等待async函数返回的Promise完成。
知道这两点之后,我们就可以知道,由于每次forEach迭代时都发起了一个异步操作,但forEach并没有等待这些异步操作完成,它会立即跳转到下一个迭代,所以这些请求是并行发出的,而不是顺序执行的。所以在第一张网络请求图中,我们看到的结果像是这些请求同步进行的。
for 循环
在使用for循环,并在其中使用了await来接收返回的结果。这里的await会让for循环暂停,直到当前的异步请求完成,才能继续下一次迭代。
具体来说就是每次循环时,await getData()Promise被解析完,才能继续执行下一次循环,这种方式保证了每个请求是按顺序依次发出的,每次等待前一个请求完成。
总结
当我们遇到需要多个请求,并且在请求完成之后再进行下一个请求,forEach循环将无法满足我们的需求,因为forEach函数内部传递的回调函数是同步执行的,并不会等待Promise的完成。所以在网络请求中看起来就像是同步进行的。
可以使用for循环来解决这个问题,因为在for循环中没有回调函数传递,它会等待每一个请求的Promise返回结果之后再进行下一个请求,因此可以解决我们的问题。