【问题标题】:What is the difference between async/await forEach and Promise.all + mapasync/await forEach 和 Promise.all + map 有什么区别
【发布时间】:2022-04-14 02:57:27
【问题描述】:

在the accepted answer to a similar question 中,答案表明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 contains only the mentioned code的输出

❯ 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

【问题讨论】:

  • 这里,除了forEach,什么都不需要。 Promise.all 用于在所有承诺运行后您确实需要做某事时。你在forEach 中的async/await 完全没有意义——类似于Promise.all,如果await 后面没有代码,await 没有意义。链接中的示例不是很好的 IMO,因为它并没有真正说明 Promise.all 的典型行为——通常你不会在其中 console.log,你会产生一系列结果然后用它做一些事情。
  • 将回调模式与 Promise 混合使用,违背了 Promise 的目的。
  • 也从那里接受的答案:“确定代码确实有效,该函数确实在此之后立即返回。”尝试添加记录"After all functions: Goodbye"的代码,当循环是完成。
  • @trincot 我试图以某种方式承诺 setTimeout,但实现起来不太抱歉
  • @AbdullahKhaled 是的,这就是 asynchronous 的意思。

标签: javascript node.js async-await promise


【解决方案1】:

对于只看到问题标题的 Google 员工

不要将 async/await 与 forEach 一起使用。要么使用for-of 循环,要么使用Promise.all() 和array.map()。我将解释它们之间的区别,但首先,关于 Promise 和 async/await 如何工作的一点理论。

如果您对 Promise 和 async/await 有一个大致的了解,那么关于 promise.all() + array.map() 和 .forEach() 之间差异的 TL;DR 是不可能等待 forEach() 本身。是的,您可以在 .forEach() 中并行运行任务,就像使用 .map() 一样,但是您不能等待所有这些并行任务完成,然后在它们全部完成后执行某些操作。使用.map() 而不是.forEach() 的全部意义在于,您可以获得一个promise 列表,使用Promise.all() 收集它们,然后等待整个过程。要明白我的意思,只需在forEach(async () => ...) 之后放置一个console.log('Finished'),您会看到"finished" 在.forEach() 循环中的所有内容运行完成之前被注销。我的建议是不要将 .forEach() 与异步逻辑一起使用(事实上,这些天没有理由再使用 .forEach(),正如我在下面进一步解释的那样)。

promise 和 async/await 入门

就本次讨论而言,您只需要记住,promise 是一个特殊对象,它承诺某项任务将在未来某个时间完成。您可以通过.then() 将侦听器附加到一个承诺,以便在任务完成时收到通知,并接收解析后的值。

async 函数只是一个无论如何都会返回承诺的函数。即使你执行async function doThing() { return 2 },它也不会返回 2,它会返回一个立即解析为值 2 的 Promise。请注意,异步函数总是会立即返回一个 Promise,即使它需要很长时间运行的功能。这就是为什么它被称为“承诺”的原因,它承诺函数最终会完成运行,如果你想在函数完成时收到通知,你可以通过.then()或@987654343向它添加一个事件监听器@。

await 是一种特殊语法,可让您暂停执行异步函数,直到 promise 解决。 await 只会影响它直接在里面的功能。在幕后,await 只是在 Promise 的 .then() 中添加了一个特殊的事件侦听器,因此它可以知道 Promise 何时解析以及它解析的值。

async fn1() {
  async 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

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()

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) 的await 不会像在前面的示例中那样导致main() 暂停,而是会导致传递给delays.map() 的回调暂停,因为这就是函数就在里面。

所以,我们有delays.map(),它将调用提供的回调,为delays 数组中的每个delay 调用一次。回调是异步的,所以它总是会立即返回一个promise。回调将使用不同的延迟参数开始执行,但不可避免地会到达await wait(delay) 行,从而暂停回调的执行。

因为.map() 的回调返回了一个promise,delays.map() 将返回一组promise,Promise.all() 将接收这些promise,并将它们组合成一个超级promise,当所有promise 都被解析时在数组中解析。现在,我们awaitpromise.all() 返回的超级承诺。这个await 在main() 内部,导致main() 暂停,直到所有提供的promise 都解决。因此,我们在main()内部创建了一堆独立的异步任务,让它们都按自己的时间完成,然后暂停main()本身的执行,直到所有这些任务都完成。

.forEach() 的问题

首先,你真的不需要使用forEach() 来做任何事情。它在for-of 循环之前就出现了,for-of 在各方面都比forEach() 好。 for-of 可以针对任何可迭代对象运行,您可以将break 和continue 与它们一起使用,最重要的是,await 将在for-of 中按预期工作,但在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.all() 将promise 组合在一起并await 它们全部。因此,您会注意到"finish" 会立即注销,因为我们不会让main() 等待这些承诺完成。这可能不是您想要的。


对原问题的具体回答

(又名原始答案)

它之所以有效,是因为 for 循环仍在运行,并且您仍然会启动一堆异步任务。问题是你没有在等待他们。当然你在.forEach() 中使用了await,但这只会导致.forEach() 回调等待,它不会暂停你的外部函数。如果您在异步 IIFE 的末尾添加 console.log(),您可以看到这一点,您的 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!')
})();

【讨论】:

  • 所以函数可以在其中的每个代码完成之前终止,对吧?
  • 正确,您当前的编码方式将导致函数在其中的所有内容完成之前终止。
猜你喜欢
  • 1970-01-01
  • 2021-08-25
  • 2017-12-30
  • 2012-03-20
  • 2021-11-05
  • 1970-01-01
  • 1970-01-01
  • 2011-11-12
相关资源
最近更新 更多