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 得到解决。这意味着awaitfor 循环中的 s 应该被串行执行。
结果正如您所期望的。
"Start";
"Apple: 27";
"Grape: 0";
"Pear: 14";
"End";
此行为适用于大多数循环(如while和for-of循环)...
但它不适用于需要回调的循环。需要回退的循环示例包括forEach、、和map。我们将在接下来的几节中探讨如何影响、和。filterreduceawaitforEachmapfilter
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";
如果您愿意,您可以操纵 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");
awaitin 的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;
使用 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";
当您使用awaitreduce 时,结果会变得非常混乱。
// 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";
什么?![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。然而,这种方法有点令人困惑,因为你必须小心操作的顺序。getNumFruitawait
在 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 循环(或任何没有回调的循环)。 - 永远不要使用
awaitwithforEach。请改用 for 循环(或任何没有回调的循环)。 - 不要
await在里面filter和reduce。始终使用,然后或相应地await包含一系列承诺。mapfilterreduce
感谢阅读。本文最初发布在我的博客上。如果您想阅读更多文章来帮助您成为更优秀的前端开发人员,请订阅我的新闻通讯。
鏂囩珷鏉ユ簮锛�https://dev.to/zellwk/javascript-async-and-await-in-loops-2g43
后端开发教程 - Java、Spring Boot 实战 - msg200.com
![控制台立即记录“开始”、“[Promise, Promise, Promise]”和“结束”](images/3.png)
![控制台立即记录“开始”、“['apple', 'grape', 'pear']”和“结束”](/upload/5-shps.png)
