对于只看到问题标题的 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!')
})();