forEach 如果传入异步回调如何保证并行执行?

简介: forEach 本身是同步的,但是如果回调函数是异步的,那么forEach 会立即执行下一个任务,而不会等待回调函数执行完毕,这个时候如何保证异步任务的串行执行呢?

forEach本身是同步的,但是如果回调函数是异步的,那么forEach会立即执行下一个任务,而不会等待回调函数执行完毕,这个时候如何保证异步任务的串行执行呢?

这是我之前写的一篇文章中提到的问题,文章地址:别再恶搞forEach了,它就是单纯的从头遍历到尾,它没有那么多问题

可恶,没有一个人回答我这个问题,所以我自己分享出来好了。

异步

提到这个问题就要先说一下异步,异步是指程序的执行顺序与代码的书写顺序不一致,比如:

console.log(1)
setTimeout(() => {
   
  console.log(2)
}, 0)
console.log(3)

这段代码的执行顺序是132,而不是123,这就是异步。

异步指的不是回调函数,回调函数是一种异步的实现方式,但是并不是所有的异步都是回调函数,比如PromiseGeneratorAsync/Await等都是异步的实现方式,但是并不是回调函数。

串行

串行是指多个任务按照顺序执行,比如:

console.log(1)
console.log(2)
console.log(3)

这段代码的执行顺序是123,这就是串行。

串行执行异步任务

那么如何保证异步任务的串行执行呢?

1. 递归

const arr = [1, 2, 3]
const asyncForEach = (arr, callback) => {
   
  const loop = (index) => {
   
    if (index === arr.length) return
    callback(arr[index], index, arr, () => {
   
      loop(index + 1)
    })
  }
  loop(0)
}

asyncForEach(arr, (item, index, arr, next) => {
   
  setTimeout(() => {
   
    console.log(item)
    next()
  }, parseInt(Math.random() * 1000))
})

这里使用了递归,每次执行完一个任务后,调用next函数,next函数会执行下一个任务。

2. Promise

const arr = [1, 2, 3]
const asyncForEach = (arr, callback) => {
   
  arr.reduce((prev, cur, index) => {
   
    return prev.then(() => {
   
      return callback(cur, index, arr)
    })
  }, Promise.resolve())
}

asyncForEach(arr, (item, index, arr) => {
   
  return new Promise((resolve) => {
   
    setTimeout(() => {
   
      console.log(item)
      resolve()
    }, parseInt(Math.random() * 1000))
  })
})

这里使用了Promise,每次执行完一个任务后,返回一个PromisePromiseresolve函数会执行下一个任务。

3. Async/Await

const arr = [1, 2, 3]
const asyncForEach = async (arr, callback) => {
   
  for (let i = 0; i < arr.length; i++) {
   
    await callback(arr[i], i, arr)
  }
}

asyncForEach(arr, async (item, index, arr) => {
   
  return new Promise((resolve) => {
   
    setTimeout(() => {
   
      console.log(item)
      resolve()
    }, parseInt(Math.random() * 1000))
  })
})

这里使用了Async/Await,每次执行完一个任务后,使用await等待下一个任务执行完毕。

forEach 中的异步如何实现串行?

上面说了那么多示例,那么forEach中的异步如何实现串行呢?

const arr = [1, 2, 3]
arr.forEach(async (item) => {
   
  setTimeout(() => {
   
    console.log(item)
  }, parseInt(Math.random() * 1000))
})

上面的代码中,forEach中的输出是随机的,你并不知道如何保证输出的顺序,那么如何保证输出的顺序呢?

1. 递归

这个时候有人就问了,forEach你怎么递归?来看看forEach递归的实现:

const arr = [1, 2, 3]
const asyncForEach = (arr) => {
   
   arr.forEach(async (item, index, arr) => {
   
      const newArr = arr.splice(index + 1, arr.length);
      setTimeout(() => {
   
         console.log(item)
         asyncForEach(newArr)
      }, parseInt(Math.random() * 1000))
   })
}
asyncForEach(arr)

这里的原理就是使用splice截取数组,这样就会修改原数组,导致forEach的每次只会迭代一次,但是缺点就是index不会发生变化,同时原数组也会发生变化。

后面两个没必要看了,就是凑字数的,和上面描述的差不多,也没用到forEach,障眼法而已。

2. Promise

const arr = [1, 2, 3]
const asyncForEach = (arr) => {
   
  arr.reduce((prev, cur, index) => {
   
    return prev.then(() => {
   
      return new Promise((resolve) => {
   
        setTimeout(() => {
   
          console.log(cur)
          resolve()
        }, parseInt(Math.random() * 1000))
      })
    })
  }, Promise.resolve())
}

asyncForEach(arr)

这里没有使用forEach,而是使用了reduce,这样就可以保证index不会发生变化,同时原数组也不会发生变化。

reduce会收集每次的Promise,最后返回一个Promise,这样就可以保证每次执行完一个任务后,再执行下一个任务。

3. Async/Await

const arr = [1, 2, 3]
const asyncForEach = async (arr) => {
   
  for (let i = 0; i < arr.length; i++) {
   
    await new Promise((resolve) => {
   
      setTimeout(() => {
   
        console.log(arr[i])
        resolve()
      }, parseInt(Math.random() * 1000))
    })
  }
}

asyncForEach(arr)

这里也没使用forEach,而是使用了for循环,这样就可以保证index不会发生变化,同时原数组也不会发生变化。

总结

异步真好玩,玩的好升职加薪,玩不好提桶走人,今天的分享就到了,不会真的有人在forEach中使用异步回调吧???

目录
相关文章
|
6月前
|
编译器 数据处理 C#
C#中的异步流:使用IAsyncEnumerable<T>和await foreach实现异步数据迭代
【1月更文挑战第10天】本文介绍了C#中异步流的概念,并通过使用IAsyncEnumerable<T>接口和await foreach语句,详细阐述了如何异步地迭代数据流。异步流为处理大量数据或需要流式处理数据的场景提供了一种高效且非阻塞性的方法,使得开发者能够更优雅地处理并发和数据流问题。
|
前端开发 JavaScript
😲完了完了,forEach异步执行,怎么后面的先完成了!?
代码review,业务里的代码千奇百怪,到底还能遇到什么呢?oh no,真的有人在forEach里用异步调用!
507 0
|
前端开发
异步转同步的几种方法
在循环等待中,我们可以使用一个变量来指示异步操作是否已完成。然后,我们可以在循环中检查该变量,如果它指示异步操作已完成,则退出循环。
552 0
|
27天前
|
前端开发 JavaScript
Async/Await 如何通过同步的方式(形式)实现异步
Async/Await 是一种在 JavaScript 中以同步方式书写异步代码的语法糖。它基于 Promise,使异步操作看起来更像顺序执行,简化了回调地狱,提高了代码可读性和维护性。
|
4月前
|
Web App开发 JavaScript 前端开发
谁说forEach不支持异步代码,只是你拿不到异步结果而已
JavaScript 的 `forEach` 不直接支持异步操作,但可以在回调中使用 `async/await`。虽然 `forEach` 不会等待 `await`,异步代码仍会执行。MDN 文档指出 `forEach` 预期同步回调。ECMAScript 规范和 V8 源码显示 `forEach` 基于 for 循环实现,不返回 Promise。通过 `setTimeout` 可观察到异步操作完成。与 `map` 不同,`forEach` 不适合处理异步序列,常需转换为 `Promise.all` 结合 `map` 的方式。
46 11
3 # 通过回调函数处理异步并发问题
3 # 通过回调函数处理异步并发问题
47 0
同步调用和异步调用
同步调用和异步调用
异步时父子线程间的ThreadLocal传递方案
异步时父子线程间的ThreadLocal传递方案
301 0
|
Java Spring 容器
你知道 @Async 是怎么让方法异步执行的吗?
@Async 是通过注解标记来开启方法的异步执行的;对于注解的底层实现,除了 java 原生提供那种依赖编译期植入的之外,其他的基本都差不多,即运行时通过反射等方式拦截到打了注解的类或者方法,然后执行时进行横切拦截;另外这里还有一个点就是方法异步执行,所以对于 @Async 的剖析,就一定绕不开两个基本的知识点,就是代理和线程池。 在了解到这些之后,我们来拆解下 @Async 的基本原理。
205 0
你知道 @Async 是怎么让方法异步执行的吗?
|
分布式计算 SpringCloudAlibaba 前端开发
JUC系列(七) ForkJion任务拆分与异步回调
ForkJion任务拆分与异步回调 也是业务中的常客了
JUC系列(七) ForkJion任务拆分与异步回调