今天研究了一下JS中的异步迭代。
考虑下面这段程序。
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(async value => {
await someRandomWork();
console.log(value);
console.log('finished!');
看起来,
forEach()
在遍历到数组中每个值时都会
await
,直到遍历完整个数组。但事实并非如此。实际的输出是这样:
finished!
5
很明显,
console.log()
跑到了最前面。不过仔细观察,可以发现
await
和
console.log()
并不在同一层。要知道,
await
是“shallow”的,里面的
await
只会suspend它所在的函数,不会对外层产生效果。
JavaScript for impatient programmers
中提到:
If we are inside an async function and want to pause it via await, we must do so directly within that function; we can’t use it inside a nested function, such as a callback. That is, pausing is shallow.
如果想在
async
函数内部“暂停”它,只能在函数的最顶层使用
await
,否则就没有效果。
看到这里,解决方案也就呼之欲出了。只要让
Promise
与后续语句成为同一层,让它们之间产生关联,就能await整个loop过程。
Promise.all()
可以将多个
Promise
合并成一个,这正好是我们想要的。因为需要一个
Promise[]
,此时
forEach()
就不太实用了,
map()
明显是一个更好的选择。将代码改写成以下形式:
async function mapAsync() {
await Promise.all(numbers.map(async i => {
await someRandomWork();
console.log(await getCurrentTime(), i);
console.log('mapAsync finished!')
}
运行,得到以下结果:
00:00:03.941 1
00:00:03.941 2
00:00:03.941 3
00:00:03.941 4
00:00:03.941 5
mapAsync finished!
为了方便观察,我在输出中增加了一个timer;同时,在
someRandomWork()
中进行一次
for (let i = 0; i < 2e9; i++)
来模拟一段时间的阻塞。此时,
console.log()
和
Promise.all()
的
await
来到了同一层,整个迭代的所有
Promise
都被resolve之后,
Promise.all()
才被resolve,从时间码也能看出过程是并行的。
mapAsync finished!
的输出也在其他的输出之后,符合预期。
既然可以通过
map()
和
Promise.all()
让所有
Promise
并行resolve,有没有办法让它们被顺序resolve呢?当然有。说到顺序,很自然就想到了
reduce()
。把代码改写成以下形式:
async function reduceAsync() {
await numbers.reduce(async (promise, i) => {
await promise;
await someRandomWork();
console.log(await getCurrentTime(), i);
}, Promise.resolve());
console.log('reduceAsync finished!')
}
reduce()
的第二个参数
Promise.resolve()
帮助我们启动这个美妙的过程。回到异步回调函数,
await promise;
确保上一个
Promise
fulfilled之后才接着进行后续的过程,直到遍历数组中的每一个元素。而传入async函数的
reduce()
本身最后返回一个
Promise
,把这个链式反应带到最外层。最后,运行的结果如下:
00:00:01.020 1
00:00:01.962 2
00:00:02.590 3
00:00:03.217 4
00:00:03.844 5
reduceAsync finished!
通过时间码可以看出,使用
reduce()
的情况下,
Promise
是被顺序resolve的。
上面两个方案都是用了高阶函数。实际上,
for...of
语句也可以当作“支持异步的
.forEach()
”来使用。使用以下代码:
async function forOfAsync() {
for (const i of numbers) {
await someRandomWork();
console.log(await getCurrentTime(), i);
console.log('forOfAsync finished!')
}
运行后可以得到以下结果:
00:00:00.992 1
00:00:01.932 2
00:00:02.560 3
00:00:03.187 4
00:00:03.814 5
forOfAsync finished!
可以看出结果和
reduce()
是类似的。如果用Babel编译上述JavaScript代码,会发现
for...of
被转换成了
for
语句,可见普通的循环在此处也是可行的。这也很合理,毕竟这里的
await
并没有被函数包裹。
for (var _i = 0, _numbers = numbers; _i < _numbers.length; _i++) {
const i = _numbers[_i];
await someRandomWork();
console.log("finished!");
如果用
for await...of
替换上面代码中的
for...of
,会发现运行结果没有什么变化。原因是,虽然带了“await”,
for await...of
并不是用在这个场景中的。
MDN
这样介绍:
When a
for await...of
loop iterates over an iterable, it first gets the iterable's
[@@asyncIterator]()
method and calls it, which returns an async iterator. If the @asyncIterator method does not exist, it then looks for an
[@@iterator]()
method, which returns a sync iterator.
for await...of
可以迭代async iterable,这是它和
for...of
在功能上最直接的区别。而且,当迭代sync iterable,并且迭代变量是async variable时,
for await...of
会返回resolved values,而
for...of
只会返回一系列Promise。下面这个例子展示了它们在面对sync iterable时的区别(
完整代码
):
const numberPromises = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3),
Promise.resolve(4),
Promise.resolve(5)
async function forOfAsync() {
for (const i of numberPromises) {
await someRandomWork();
console.log(await getCurrentTime(), i);
console.log('forOfAsync finished!')
async function forAwaitOfAsync() {
for await (const i of numberPromises) {
await someRandomWork();
console.log(await getCurrentTime(), i);
console.log('forAwaitOfAsync finished!')
async function main() {
console.log(`\nfor...of test`);
await forOfAsync();
console.log(`\nfor await...of test`);
await forAwaitOfAsync();
main();
// expected output:
// for...of test
// 00:00:00.988 Promise { 1 }
// 00:00:01.933 Promise { 2 }
// 00:00:02.562 Promise { 3 }
// 00:00:03.189 Promise { 4 }
// 00:00:03.816 Promise { 5 }
// forOfAsync finished!
// for await...of test
// 00:00:00.627 1
// 00:00:01.254 2
// 00:00:01.882 3
// 00:00:02.509 4
// 00:00:03.136 5
// forAwaitOfAsync finished!
此外,
MDN上的这个例子
展示了
for await...of
迭代async iterable的行为。
你可能会注意到
Promise.all()
比顺序执行的方案稍微慢一点。如果你在意性能的话,可以试试
这个map和for...of的例子
。在笔者的环境(M1,Node.js v16.17.0-arm64)下,这个例子展现出了相当有趣的差异。
参考链接:
-
javascript - Using async/await with a forEach loop - Stack Overflow
-
Async functions • JavaScript for impatient programmers (ES2022 edition)
-
tc39/proposal-async-iteration: Asynchronous iteration for JavaScript
-
for await...of - JavaScript | MDN
本文链接:
https://www.direcore.xyz/archives/35/