JavaScript 循环中的 async 和 await
基本async
和简单。当你尝试使用循环时,await
事情会变得有点复杂。await
await
在本文中,我想分享一些在循环中使用时需要注意的陷阱。
开始之前
我假设你知道如何使用async
和await
。如果你不知道,请先阅读上一篇文章,熟悉后再继续。
准备一个例子
对于本文,假设您想获取水果篮中的水果数量。
const fruitBasket = {
apple: 27,
grape: 0,
pear: 14
};
你想从水果篮中获取每种水果的数量。要获取水果的数量,可以使用getNumFruit
函数。
const getNumFruit = fruit => {
return fruitBasket[fruit];
};
const numApples = getNumFruit("apple");
console.log(numApples); // 27
现在,假设fruitBasket
远程服务器上有一个 live 对象。访问该对象需要一秒钟。我们可以用超时来模拟这一秒的延迟。(如果您对超时代码理解有困难,请参阅上一篇文章)。
const sleep = ms => {
return new Promise(resolve => setTimeout(resolve, ms));
};
const getNumFruit = fruit => {
return sleep(1000).then(v => fruitBasket[fruit]);
};
getNumFruit("apple").then(num => console.log(num)); // 27
最后,假设您想使用await
并getNumFruit
在异步函数中获取每种水果的数量。
const control = async _ => {
console.log("Start");
const numApples = await getNumFruit("apple");
console.log(numApples);
const numGrapes = await getNumFruit("grape");
console.log(numGrapes);
const numPears = await getNumFruit("pear");
console.log(numPears);
console.log("End");
};

有了这个,我们就可以开始await
循环查看了。
在 for 循环中等待
假设我们有一组想要从水果篮中取出的水果。
const fruitsToGet = ["apple", "grape", "pear"];
我们将循环遍历这个数组。
const forLoop = async _ => {
console.log("Start");
for (let index = 0; index < fruitsToGet.length; index++) {
// Get num of each fruit
}
console.log("End");
};
在 for 循环中,我们将使用它getNumFruit
来获取每个水果的数量。我们还会将该数量记录到控制台中。
由于getNumFruit
返回一个承诺,我们可以await
在记录它之前解析值。
const forLoop = async _ => {
console.log("Start");
for (let index = 0; index < fruitsToGet.length; index++) {
const fruit = fruitsToGet[index];
const numFruit = await getNumFruit(fruit);
console.log(numFruit);
}
console.log("End");
};
当你使用 时await
,你期望 JavaScript 暂停执行,直到等待的 Promise 得到解决。这意味着await
for 循环中的 s 应该被串行执行。
结果正如您所期望的。
"Start";
"Apple: 27";
"Grape: 0";
"Pear: 14";
"End";

此行为适用于大多数循环(如while
和for-of
循环)...
但它不适用于需要回调的循环。需要回退的循环示例包括forEach
、、和map
。我们将在接下来的几节中探讨如何影响、和。filter
reduce
await
forEach
map
filter
forEach 循环中的 Await
我们将执行与 for 循环示例中相同的操作。首先,让我们循环遍历水果数组。
const forEachLoop = _ => {
console.log("Start");
fruitsToGet.forEach(fruit => {
// Send a promise for each fruit
});
console.log("End");
};
接下来,我们将尝试使用 来获取水果的数量getNumFruit
。(注意async
回调函数中的关键字。我们需要这个async
关键字,因为await
位于回调函数中)。
const forEachLoop = _ => {
console.log("Start");
fruitsToGet.forEach(async fruit => {
const numFruit = await getNumFruit(fruit);
console.log(numFruit);
});
console.log("End");
};
您可能希望控制台看起来像这样:
"Start";
"27";
"0";
"14";
"End";
但实际结果却不同。JavaScriptconsole.log('End')
在 forEach 循环中的 promise 得到解决之前就继续调用。
控制台按以下顺序记录:
'Start'
'End'
'27'
'0'
'14'

JavaScript 之所以这样做,是因为forEach
不支持 Promise。它不支持async
和await
。你不能await
在 中使用forEach
。
等待地图
await
如果在 a 中使用map
,map
将始终返回一个 Promise 数组。这是因为异步函数始终返回 Promise。
const mapLoop = async _ => {
console.log("Start");
const numFruits = await fruitsToGet.map(async fruit => {
const numFruit = await getNumFruit(fruit);
return numFruit;
});
console.log(numFruits);
console.log("End");
};
"Start";
"[Promise, Promise, Promise]";
"End";
由于map
总是返回 Promise(如果你使用await
),你必须等待 Promise 数组得到解决。你可以使用 来做到这一点await Promise.all(arrayOfPromises)
。
const mapLoop = async _ => {
console.log("Start");
const promises = fruitsToGet.map(async fruit => {
const numFruit = await getNumFruit(fruit);
return numFruit;
});
const numFruits = await Promise.all(promises);
console.log(numFruits);
console.log("End");
};
您将获得以下内容:
"Start";
"[27, 0, 14]";
"End";
![控制台输出“Start”。一秒钟后,它输出“[27, 0, 14]”和“End”。](/upload/4-fsxd.gif)
如果您愿意,您可以操纵 Promise 中返回的值。解析后的值将是您返回的值。
const mapLoop = async _ => {
// ...
const promises = fruitsToGet.map(async fruit => {
const numFruit = await getNumFruit(fruit);
// Adds onn fruits before returning
return numFruit + 100;
});
// ...
};
"Start";
"[127, 100, 114]";
"End";
使用过滤器等待
当你使用 时filter
,你希望用特定的结果来过滤一个数组。假设你想创建一个包含超过 20 个水果的数组。
如果你filter
正常使用(不带 await),你会像这样使用它:
// Filter if there's no await
const filterLoop = _ => {
console.log('Start')
const moreThan20 = await fruitsToGet.filter(fruit => {
const numFruit = fruitBasket[fruit]
return numFruit > 20
})
console.log(moreThan20)
console.log('End')
}
您可能认为moreThan20
其中只包含苹果,因为有 27 个苹果,但有 0 个葡萄和 14 个梨。
"Start"["apple"];
("End");
await
in 的filter
工作方式不一样。事实上,它根本不起作用。你得到的是未过滤的数组……
const filterLoop = _ => {
console.log('Start')
const moreThan20 = await fruitsToGet.filter(async fruit => {
const numFruit = getNumFruit(fruit)
return numFruit > 20
})
console.log(moreThan20)
console.log('End')
}
"Start"[("apple", "grape", "pear")];
("End");
这就是发生这种情况的原因。
当你在回调await
中使用时filter
,回调始终是一个 Promise。由于 Promise 始终为真,因此数组中的所有内容都会通过过滤器。await
在回调中使用 Promisefilter
就像编写以下代码:
// Everything passes the filter...
const filtered = array.filter(true);
await
正确使用有三个步骤filter
:
- 用于
map
返回数组承诺 await
一系列的承诺filter
解析值
const filterLoop = async _ => {
console.log("Start");
const promises = await fruitsToGet.map(fruit => getNumFruit(fruit));
const numFruits = await Promise.all(promises);
const moreThan20 = fruitsToGet.filter((fruit, index) => {
const numFruit = numFruits[index];
return numFruit > 20;
});
console.log(moreThan20);
console.log("End");
};
Start["apple"];
End;
![控制台显示“Start”。一秒钟后,控制台输出“['apple']”和“End”](/upload/6-shzu.gif)
使用 reduce 等待
假设你想找出水果盒(fruitBastet)里水果的总数。通常情况下,你可以使用reduce
循环遍历数组并计算总数。
// Reduce if there's no await
const reduceLoop = _ => {
console.log("Start");
const sum = fruitsToGet.reduce((sum, fruit) => {
const numFruit = fruitBasket[fruit];
return sum + numFruit;
}, 0);
console.log(sum);
console.log("End");
};
您将获得总共 41 个水果。(27 + 0 + 14 = 41)。
"Start";
"41";
"End";
当您使用await
reduce 时,结果会变得非常混乱。
// Reduce if we await getNumFruit
const reduceLoop = async _ => {
console.log("Start");
const sum = await fruitsToGet.reduce(async (sum, fruit) => {
const numFruit = await getNumFruit(fruit);
return sum + numFruit;
}, 0);
console.log(sum);
console.log("End");
};
"Start";
"[object Promise]14";
"End";
![控制台输出“Start”。一秒钟后,它输出“[object Promise]14”和“End”](/upload/8-qxin.gif)
什么?![object Promise]14
?!
剖析这一点很有趣。
- 在第一次迭代中,
sum
是0
。numFruit
是 27(从 解析出的值getNumFruit('apple')
)。0 + 27
是 27。 - 在第二次迭代中,
sum
是一个 Promise。(为什么?因为异步函数总是返回 Promise!)为0。PromisenumFruit
通常无法添加到对象,因此 JavaScript 会将其转换为[object Promise]
字符串。[object Promise] + 0
[object Promise]0
- 在第三次迭代中,
sum
也是一个承诺。numFruit
是14
。[object Promise] + 14
是[object Promise]14
。
谜团解开了!
这意味着,您可以在回调await
中使用reduce
,但您必须await
先记住累加器!
const reduceLoop = async _ => {
console.log("Start");
const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => {
const sum = await promisedSum;
const numFruit = await getNumFruit(fruit);
return sum + numFruit;
}, 0);
console.log(sum);
console.log("End");
};
"Start";
"41";
"End";

但是……正如你从动图上看到的,所有操作都耗时很长await
。这是因为每次迭代都reduceLoop
需要等待完成。promisedSum
有一种方法可以加速 reduce 循环。(感谢Tim Oxley我发现了这个)。如果你await getNumFruits()
先执行await promisedSum
,则reduceLoop
只需一秒钟即可完成:
const reduceLoop = async _ => {
console.log("Start");
const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => {
// Heavy-lifting comes first.
// This triggers all three `getNumFruit` promises before waiting for the next interation of the loop.
const numFruit = await getNumFruit(fruit);
const sum = await promisedSum;
return sum + numFruit;
}, 0);
console.log(sum);
console.log("End");
};

这种方法之所以有效,是因为可以在等待循环的下一次迭代之前reduce
触发所有三个Promise。然而,这种方法有点令人困惑,因为你必须小心操作的顺序。getNumFruit
await
在 reduce 中使用最简单(也是最有效)的方法await
如下:
- 用于
map
返回数组承诺 await
一系列的承诺reduce
解析值
const reduceLoop = async _ => {
console.log("Start");
const promises = fruitsToGet.map(getNumFruit);
const numFruits = await Promise.all(promises);
const sum = numFruits.reduce((sum, fruit) => sum + fruit);
console.log(sum);
console.log("End");
};
此版本简单易读,只需一秒钟即可计算出水果总数。

关键要点
- 如果您想
await
连续执行调用,请使用 for 循环(或任何没有回调的循环)。 - 永远不要使用
await
withforEach
。请改用 for 循环(或任何没有回调的循环)。 - 不要
await
在里面filter
和reduce
。始终使用,然后或相应地await
包含一系列承诺。map
filter
reduce
感谢阅读。本文最初发布在我的博客上。如果您想阅读更多文章来帮助您成为更优秀的前端开发人员,请订阅我的新闻通讯。
鏂囩珷鏉ユ簮锛�https://dev.to/zellwk/javascript-async-and-await-in-loops-2g43