在forEach循环中使用async/await有什么问题吗?我正在尝试循环浏览一系列文件,并等待每个文件的内容。
import fs from 'fs-promise'
async function printFiles () {
const files = await getFilePaths() // Assume this works fine
files.forEach(async (file) => {
const contents = await fs.readFile(file, 'utf8')
console.log(contents)
})
}
printFiles()
这段代码确实有效,但这段代码会出错吗?我有人告诉我,你不应该在这样的高阶函数中使用async/await,所以我只想问问这是否有问题。
正如其他答案所提到的,您可能希望它按顺序而不是并行执行。即,运行第一个文件,等待完成,然后一旦完成,运行第二个文件。这不会发生。
我认为很重要的是要解决为什么没有发生这种情况。
想想forEach是如何工作的。我找不到来源,但我认为它的工作原理如下:
const forEach = (arr, cb) => {
for (let i = 0; i < arr.length; i++) {
cb(arr[i]);
}
};
现在想想当你做这样的事情时会发生什么:
forEach(files, async logFile(file) {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
});
在forEach的for循环中,我们调用cb(arr[i]),最后是logFile(file)。logFile函数内部有一个await,所以for循环可能会在继续到i++之前等待这个await?
不,不会的。令人困惑的是,这不是wait的工作方式。从文档中:
await分割执行流,允许异步函数的调用方继续执行。在await延迟异步函数的继续之后,随后执行后续语句。如果此await是其函数执行的最后一个表达式,则继续执行,方法是向函数的调用方返回完成await函数的未决Promise并继续执行该调用方。
因此,如果您有以下内容,则不会在“b”之前记录数字:
const delay = (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
const logNumbers = async () => {
console.log(1);
await delay(2000);
console.log(2);
await delay(2000);
console.log(3);
};
const main = () => {
console.log("a");
logNumbers();
console.log("b");
};
main();
循环回到forEach,forEach就像main,logFile就像logNumbers。main不会因为logNumbers等待而停止,forEach不会因为logFile等待而停止。
正如其他答案所提到的,您可能希望它按顺序而不是并行执行。即,运行第一个文件,等待完成,然后一旦完成,运行第二个文件。这不会发生。
我认为很重要的是要解决为什么没有发生这种情况。
想想forEach是如何工作的。我找不到来源,但我认为它的工作原理如下:
const forEach = (arr, cb) => {
for (let i = 0; i < arr.length; i++) {
cb(arr[i]);
}
};
现在想想当你做这样的事情时会发生什么:
forEach(files, async logFile(file) {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
});
在forEach的for循环中,我们调用cb(arr[i]),最后是logFile(file)。logFile函数内部有一个await,所以for循环可能会在继续到i++之前等待这个await?
不,不会的。令人困惑的是,这不是wait的工作方式。从文档中:
await分割执行流,允许异步函数的调用方继续执行。在await延迟异步函数的继续之后,随后执行后续语句。如果此await是其函数执行的最后一个表达式,则继续执行,方法是向函数的调用方返回完成await函数的未决Promise并继续执行该调用方。
因此,如果您有以下内容,则不会在“b”之前记录数字:
const delay = (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
const logNumbers = async () => {
console.log(1);
await delay(2000);
console.log(2);
await delay(2000);
console.log(3);
};
const main = () => {
console.log("a");
logNumbers();
console.log("b");
};
main();
循环回到forEach,forEach就像main,logFile就像logNumbers。main不会因为logNumbers等待而停止,forEach不会因为logFile等待而停止。
如果要同时迭代所有元素:
async function asyncForEach(arr, fn) {
await Promise.all(arr.map(fn));
}
如果您希望非并发地遍历所有元素(例如,当映射函数具有副作用或同时在所有数组元素上运行mapper时,资源成本太高):
选项A:承诺
function asyncForEachStrict(arr, fn) {
return new Promise((resolve) => {
arr.reduce(
(promise, cur, idx) => promise
.then(() => fn(cur, idx, arr)),
Promise.resolve(),
).then(() => resolve());
});
}
选项B:异步/等待
async function asyncForEachStrict(arr, fn) {
for (let idx = 0; idx < arr.length; idx += 1) {
const cur = arr[idx];
await fn(cur, idx, arr);
}
}
在一个文件中弹出几个方法,以串行化的顺序处理异步数据,并为代码提供更传统的风格,这是非常轻松的。例如:
module.exports = function () {
var self = this;
this.each = async (items, fn) => {
if (items && items.length) {
await Promise.all(
items.map(async (item) => {
await fn(item);
}));
}
};
this.reduce = async (items, fn, initialValue) => {
await self.each(
items, async (item) => {
initialValue = await fn(initialValue, item);
});
return initialValue;
};
};
现在,假设保存在'/myAsync.js'您可以在相邻文件中执行类似以下操作:
...
/* your server setup here */
...
var MyAsync = require('./myAsync');
var Cat = require('./models/Cat');
var Doje = require('./models/Doje');
var example = async () => {
var myAsync = new MyAsync();
var doje = await Doje.findOne({ name: 'Doje', noises: [] }).save();
var cleanParams = [];
// FOR EACH EXAMPLE
await myAsync.each(['bork', 'concern', 'heck'],
async (elem) => {
if (elem !== 'heck') {
await doje.update({ $push: { 'noises': elem }});
}
});
var cat = await Cat.findOne({ name: 'Nyan' });
// REDUCE EXAMPLE
var friendsOfNyanCat = await myAsync.reduce(cat.friends,
async (catArray, friendId) => {
var friend = await Friend.findById(friendId);
if (friend.name !== 'Long cat') {
catArray.push(friend.name);
}
}, []);
// Assuming Long Cat was a friend of Nyan Cat...
assert(friendsOfNyanCat.length === (cat.friends.length - 1));
}
此解决方案还对内存进行了优化,因此您可以在10000个数据项和请求上运行它。这里的一些其他解决方案将使大型数据集上的服务器崩溃。
在TypeScript中:
export async function asyncForEach<T>(array: Array<T>, callback: (item: T, index: number) => Promise<void>) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index);
}
}
如何使用?
await asyncForEach(receipts, async (eachItem) => {
await ...
})