异步请求中的循序渐进:for 和 forEach 之间的关键区别

445 阅读4分钟

前言

今天在公司写代码时遇到了一个场景:

通过在网页端上传发票文件到服务端,由服务端进行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进行接收返回的数据。

我的期望是每一次请求完成并且得到数据之后再进行下一个请求,但是在网络请求中是这样的:

image.png 从上图中可以看到,网络请求分为两段,因为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()

网络请求的结果如下:

image.png

可以看到,网络请请求是一条直线,那就说明一次请求完毕之后再进行下一个请求,也就是我期望的效果。

原因

下面我将介绍这两种循环的区别。

forEach

当使用forEach循环时,并在其中调用了async函数,要理解为什么出现异步请求“同步”的问题,就要知道两点

  1. forEach是同步执行的,也就是说它会一次性的遍历完数组的所有元素。
  2. forEach并不等待async函数执行完毕之后再继续下一次迭代,即使回调函数内有async函数,forEach仍然会立即执行下一个迭代,而不等待async函数返回的Promise完成。

知道这两点之后,我们就可以知道,由于每次forEach迭代时都发起了一个异步操作,但forEach并没有等待这些异步操作完成,它会立即跳转到下一个迭代,所以这些请求是并行发出的,而不是顺序执行的。所以在第一张网络请求图中,我们看到的结果像是这些请求同步进行的。

for 循环

在使用for循环,并在其中使用了await来接收返回的结果。这里的await会让for循环暂停,直到当前的异步请求完成,才能继续下一次迭代。

具体来说就是每次循环时,await getData()Promise被解析完,才能继续执行下一次循环,这种方式保证了每个请求是按顺序依次发出的,每次等待前一个请求完成。

总结

当我们遇到需要多个请求,并且在请求完成之后再进行下一个请求,forEach循环将无法满足我们的需求,因为forEach函数内部传递的回调函数是同步执行的,并不会等待Promise的完成。所以在网络请求中看起来就像是同步进行的。

可以使用for循环来解决这个问题,因为在for循环中没有回调函数传递,它会等待每一个请求的Promise返回结果之后再进行下一个请求,因此可以解决我们的问题。