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

724 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情

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中使用异步回调吧???