在 forEach 使用 async/await 的问题

简介: 在 forEach 使用 async/await 的问题

前言


提一下在 Array.prototype.forEach() 使用 async/await 的问题。其实,在 MDN 上就有提醒:


如果使用 promiseasync 函数作为 forEach() 等类似方法的 callback 参数,最好对造成的执行顺序影响多加考虑,否则容易出现错误。


正文


示例:

let sum = 0
const arr = [1, 2, 3]
async function sumFn(a, b) {
  return a + b
}
// 为了方便后续修正改造,将 forEach 逻辑放到函数 main 中执行了。
function main(array) {
  array.forEach(async item => {
    sum = await sumFn(sum, item)
  })
  console.log(sum) // 0, Why?
}
main(arr)


为什么 sum 打印结果是 0,而不是预期的 6 呢?


首先,我们要理解 async 函数的语义,它表示函数中有异步操作,await 则表示其后面的表达式需要等待结果,函数最终返回一个 Promise 对象。


当代码执行到 forEach 时:

1. 首先遇到 `sum = await sumFn(sum, item)` 语句(注意,它是从右往左执行的)
   因此,它会执行 `sumFn(0, 1)`,那么该函数 `return 1`,
   由于 async 函数始终会返回一个 Promise 对象,即 `return Promise.resolve(1)`。
2. 由于 await 的原因,它其实相当于执行 `Promise.resolve(3).then()` 方法,
   它属于微任务,会暂时 Hold 住,被放入微任务的队列,待本次同步任务执行完之后,
   才会被执行,因此并不会立即赋值给 sum(所以 sum 仍为 0)。
3. 那 JS 引擎主线程不会闲着的,它会继续执行“同步任务”,即下一次循环。
   同理,又将 `return Promise.resolve(2)` 放入微任务队列。
   直到最后一次循环,同样的的还是 `return Promise.resolve(3)`。
   其实到这里,forEach 其实算是执行完了。
   以上示例,forEach 的真正意义是创建了 3 个微任务。
4. 由于主线程会一直执行同步任务,待同步任务执行完之后,才会执行任务队列里面的微任务。
   待 forEach 循环结束之后,自然会执行 `console.log(sum)`,
   但注意,由于 await 的原因,sum 一直未被重新赋值,因此 sum 还是为 0 ,
   所以控制台输出了 0。
5. 等 `console.log(sum)` 执行完毕,才开始执行队列中的微任务,
   其中 `await Promise.resolve(0)` 的结果,
   相当于 `Promise.resolve(0)` 的 then 方法的返回值,
   所以此前的三个微任务,相当于:
   `sum = 1`
   `sum = 2`
   `sum = 3`
   它们被依次执行。
6. 因此 sum 最终的值变成了 3(注意不是 6 哦)。


所以,在 forEach 中使用 async/await 可能没办法到达预期目的哦。

如何解决以上问题呢?


我们可以使用 for...of 来替代:

let sum = 0
const arr = [1, 2, 3]
async function sumFn(a, b) {
  return a + b
}
// await 要放在 async 函数中
async function main(array) {
  for (let item of array) {
    sum = await sumFn(sum, item)
  }
  console.log(sum) // 6
}
main(arr)


这样就能输出预期结果 6 了。


那为什么 for...of 就可以呢?因为它本质上就是一个 while 循环。

let sum = 0
const arr = [1, 2, 3]
async function sumFn(a, b) {
  return a + b
}
// await 要放在 async 函数中
async function main(array) {
  // for (let item of array) {
  //   sum = await sumFn(sum, item)
  // }
  // 相当于
  const iterator = array[Symbol.iterator]()
  let iteratorResult = iterator.next()
  while (!iteratorResult.done) {
    sum = await sumFn(sum, iteratorResult.value)
    iteratorResult = iterator.next()
  }
  console.log(sum) // 6
}
main(arr)


只要了解了 async/awaitfor...of 的内部运行机制,分析起来就不难了。


The end.


目录
相关文章
|
SQL 存储 缓存
MySQL - 一文了解MySQL的基础架构及各个组件的作用
MySQL - 一文了解MySQL的基础架构及各个组件的作用
1141 0
|
JavaScript
Vue中 使用 moment.js 计算时间差值
Vue中 使用 moment.js 计算时间差值
1291 0
Vue中 使用 moment.js 计算时间差值
|
SQL XML Java
mybatis之动态SQL常见标签的使用
mybatis之动态SQL常见标签的使用
326 0
|
12月前
|
机器学习/深度学习 人工智能 云计算
与阿里合作项目荣获2024年度教育部产学合作协同育人项目优秀案例
该项目强调利用阿里云计算有限公司的低代码开发平台和算力资源,开发创新性的教学案例,以支持机器学习和深度学习等前沿技术课程的教学和实验。项目部分成果纳入了即将出版的《深度学习实战案例》教材中,该教材由人民邮电出版社出版。
593 10
|
数据采集 监控 安全
数据预处理几种常见问题
【6月更文挑战第12天】数据处理中常见的问题:数据缺失、数据重复、数据异常和数据样本差异大。对于数据缺失,处理方法包括定位、不处理、删除和填补,其中填补可使用业务知识、其他属性或统计方法。
|
缓存 JavaScript
请问如何在 keep-alive 组件中设置缓存的最大数量和过期时间
请问如何在 keep-alive 组件中设置缓存的最大数量和过期时间
|
Java Spring
mybatisplus的typeAliasesPackage 配置
【6月更文挑战第20天】mybatisplus的typeAliasesPackage 配置
1528 3
MybatisPlus--IService接口基本用法,MP提供了Service接口,save(T) 这里的意思是新增了一个T, saveBatch 是批量新增的意思,saveOrUpdate是增或改
MybatisPlus--IService接口基本用法,MP提供了Service接口,save(T) 这里的意思是新增了一个T, saveBatch 是批量新增的意思,saveOrUpdate是增或改
|
JSON 资源调度 前端开发
vue-quill使用时候要注意css引入方式,不然会报错,卡死,白屏
vue-quill使用时候要注意css引入方式,不然会报错,卡死,白屏
846 0
|
JSON 数据格式
糊涂工具类(hutool)post请求设置body参数为json数据
糊涂工具类(hutool)post请求设置body参数为json数据

热门文章

最新文章