async/await forEach和Promise.all + map有什么区别?

6
在类似问题的被采纳的答案中,回答说一个forEach调用只是抛出一个承诺然后退出。我认为这应该是这样的,因为forEach返回undefined,但为什么以下代码能够运行?
const networkFunction = (callback) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(callback());
    }, 200);
  });
};

(async () => {
  const numbers = [0, 1, 2];
  // works in parallel
  numbers.forEach(async (num) => {
    await networkFunction(() => {
      console.log("For Each Function: Hello");
    });
  });
})();

它可以并行工作,以下是time node main.js # main.js 只包含所提到的代码的输出结果。

❯ time node main.js
For Each Function: Hello
For Each Function: Hello
For Each Function: Hello

________________________________________________________
Executed in  365.63 millis    fish           external
   usr time  126.02 millis  964.00 micros  125.05 millis
   sys time   36.68 millis  618.00 micros   36.06 millis

2
这里,除了 forEach 之外不需要任何东西。当你实际上需要在所有的 promises 运行后执行某些操作时,才会使用 Promise.all。在此处使用 async/await 是完全没有意义的,类似于 Promise.all,如果在 await 后面没有代码,那么就没有必要使用它。我认为链接中的示例并不是很好,因为它并没有真正说明 Promise.all 的典型行为——通常你不会在其中使用 console.log,而是会生成一个结果数组,然后对其进行处理。 - ggorlen
4
将回调模式与Promise混合使用,会破坏Promise的意义。 - trincot
2
从已接受的答案中可以看到:“当然,代码是有效的,该函数在此之后立即返回”。尝试添加代码,在循环完成时记录“After all functions: Goodbye”。 - Bergi
@trincot 我试图以某种方式将setTimeout转换为Promise,但实现并不完美,对此我很抱歉。 - Abdullah Khaled
@Bergi 为了确保我理解你的意思,你是指一个函数可以在其中现有的代码仍在运行时终止吗? - Abdullah Khaled
1
@AbdullahKhaled 是的,这就是异步的意思。 - Bergi
1个回答

36
O.P.的问题是在询问另一个StackOverflow问题在这里找到的澄清。如需进一步阅读以及关于这个普遍话题的许多其他很棒的答案,请查看链接。
对于只看到问题标题的谷歌用户
不要在forEach中使用async/await。要么使用for-of循环,要么使用Promise.all()与array.map()。
如果你对Promise和async/await有一个基本的了解,那么关于promise.all() + array.map().forEach()之间的区别的简短总结是,无法等待forEach()本身。是的,在.forEach()中可以像在.map()中一样并行运行任务,但你不能等待所有这些并行任务完成,然后再执行某些操作。使用.map()而不是.forEach()的整个目的是为了获取一组promises,并使用Promise.all()来收集它们,然后等待整个过程完成。要理解我的意思,只需在forEach(async () => ...)之后加上console.log('Finished'),你会看到"finished".forEach()循环中的所有任务都完成之前就被记录出来了。我的建议是不要在异步逻辑中使用.forEach()(实际上,现在几乎没有理由再使用.forEach(),我会在下面进一步解释)。
对于那些需要更深入了解的人来说,本答案的其余部分将更详细地介绍,首先对承诺进行简要回顾,然后详细解释这些方法的行为差异以及为什么在处理异步/等待时,.forEach()始终是较差的解决方案。
关于承诺和异步/等待的基础知识
对于本讨论而言,您只需记住,承诺是一个特殊的对象,它承诺某个任务将在将来的某个时间点完成。您可以通过.then()附加监听器到承诺上,以在任务完成时收到通知,并接收解析后的值。
一个async函数只是一个无论如何都会返回一个promise的函数。即使你写了async function doThing() { return 2 },它不会返回2,而是会返回一个立即解析为值2的promise。请注意,异步函数总是会立即返回一个promise,即使函数运行需要很长时间。这就是为什么它被称为“promise”,它承诺函数最终会完成运行,如果你想在函数完成后得到通知,可以通过.then()await添加事件监听器。 await是一种特殊的语法,它允许你暂停执行异步函数直到一个promise解析。await只会影响直接包含它的函数。在幕后,await只是简单地向promise的.then()添加了一个特殊的事件监听器,以便知道promise何时解析以及解析的值是什么。
async function fn1() {
  async function fn2() {
    await myPromise // This pauses execution of fn2(), not fn1()!
  }
  ...
}

async function fn1() {
  function fn2() {
    await myPromise // An error, because fn2() is not async.
  }
  ...
}

如果你能很好地掌握这些原则,那么你应该能够理解接下来的部分。 for-of 循环允许你按顺序执行异步任务,一个接一个地。例如:

const delays = [1000, 1400, 1200];

// A function that will return a
// promise that resolves after the specified
// amount of time.
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))

async function main() {
  console.log('start')
  for (const delay of delays) {
    await wait(delay)
    console.log('Finished waiting for the delay ' + delay)
  }
  console.log('finish')
}

main()

await 会导致 main() 在指定的延迟后暂停,然后循环继续执行,console.log() 被执行,然后循环开始下一次迭代,进入新的延迟。
这个应该比较简单明了。 Promise.all()array.map() 的结合使用可以有效地并行运行多个异步任务,例如,我们可以等待多个不同的延迟同时完成。

const delays = [1000, 1400, 1200];

// A function that will return a
// promise that resolves after the specified
// amount of time.
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))

async function main() {
  console.log('start')
  await Promise.all(delays.map(async delay => {
    await wait(delay)
    console.log('Finished waiting for the delay ' + delay)
  }))
  console.log('finish')
}

main()

如果你还记得我们关于Promise和async/await的简要介绍,你会记得await只会影响到直接包含它的函数,使得该函数暂停执行。在这个例子中,await wait(delay)不会像之前的例子那样导致main()暂停执行,而是会导致传递给delays.map()的回调函数暂停执行,因为它直接包含在这个函数内部。
所以,我们有一个delays.map(),它会对delays数组中的每个delay调用提供的回调函数。这个回调函数是异步的,所以它总是会立即返回一个Promise。回调函数将使用不同的延迟参数开始执行,但最终会遇到await wait(delay)这一行代码,从而暂停回调函数的执行。
因为.map()的回调函数返回一个promise,所以delays.map()将返回一个promise数组,Promise.all()将接收这些promise,并将它们合并成一个超级promise,当数组中的所有promise都解决时,该超级promise将被解决。现在,我们awaitpromise.all()返回的超级promise。这个await位于main()内部,导致main()暂停,直到提供的所有promise都解决。因此,我们在main()内部创建了一堆独立的异步任务,让它们按照自己的时间完成,然后暂停了main()本身的执行,直到所有这些任务都完成。

.forEach()的问题

首先,你真的不需要使用forEach()。它在for-of循环出现之前就已经存在了,而且for-of在各个方面都比forEach()更好。for-of可以对任何可迭代对象进行遍历,你可以在其中使用breakcontinue,而且最重要的是,在for-ofawait会按预期工作,但在forEach()中不会。下面是原因:

const delays = [1000, 1400, 1200];

// A function that will return a
// promise that resolves after the specified
// amount of time.
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))

async function main() {
  console.log('start')
  delays.forEach(async delay => {
    await wait(delay)
    console.log('Finished waiting for the delay ' + delay)
  })
  console.log('finish')
}

main()

首先,你会注意到.forEach()会导致任务并行运行,而不是像.map()一样串行运行。这是因为forEach()内部的await只影响回调函数,而不影响main()。所以,当我们运行delays.forEach()时,我们为delays中的每个delay调用这个异步函数,启动了一系列的异步任务。问题是没有任何东西等待异步任务完成,事实上,等待它们完成是不可能的。异步回调函数每次被调用时返回一个promise,但与.map()不同,.forEach()完全忽略其回调的返回值。.forEach()接收到promise后,直接忽略它。这使得无法将这些promise分组并通过Promise.all()一起等待和await,就像之前做的那样。因此,你会注意到"finish"立即被记录出来,因为我们从未让main()等待这些promise完成。这可能不是你想要的。


原问题的具体答案

(也就是原始答案)

它能工作是因为for循环仍然运行,你仍然启动了一堆异步任务。问题在于你没有等待它们。当然,你在.forEach()内部使用了await,但这只会导致.forEach()回调等待,而不会暂停外部函数。如果在异步IIFE的末尾放置一个console.log(),你会发现在所有请求完成之前,控制台日志会立即触发。如果你使用Promise.all()代替,那么这个console.log()将会在请求完成后触发。

错误版本:

const networkFunction = (callback) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(callback());
    }, 200);
  });
};

(async () => {
  const numbers = [0, 1, 2];
  // works in parallel
  numbers.forEach(async (num) => {
    await networkFunction(() => {
      console.log("For Each Function: Hello");
    });
  });
  console.log('All requests finished!')
})();

修复版本:

const networkFunction = (callback) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(callback());
    }, 200);
  });
};

(async () => {
  const numbers = [0, 1, 2];
  // works in parallel
  await Promise.all(numbers.map(async (num) => {
    await networkFunction(() => {
      console.log("For Each Function: Hello");
    });
  }));
  console.log('All requests finished!')
})();


所以这个函数可以在它内部的所有代码都执行完之前终止,对吗? - Abdullah Khaled
正确,你目前编写的代码会导致函数在完成所有操作之前终止。 - Scotty Jamison
2
多么惊人的答案。谢谢Scotty。 - Alex MAN
1
同意。非常棒的答案。 - Danoz
1
非常好的回答。这个回答应该比它现在拥有的赞更多。 - Daniyal Malik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接