在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,所以我只想问问这是否有问题。
files.forEach(异步(文件)=>{const contents=await fs.readFile(文件,'utf8')})
问题是,forEach()忽略了迭代函数返回的promise。在每个异步代码执行完成后,forEach不会等待移动到下一个迭代。所有fs.readFile函数将在同一轮事件循环中调用,这意味着它们是并行启动的,而不是顺序启动的,并且在调用forEach()后立即继续执行等待所有fs.readFile操作完成。由于forEach不等待每个promise解析,因此循环实际上在解析promise之前完成了迭代。您希望在forEach完成后,所有异步代码都已执行,但事实并非如此。您最终可能会尝试访问尚未可用的值。
您可以使用以下示例代码测试行为
常量数组=[1,2,3];const sleep=(ms)=>new Promise(解析=>setTimeout(解析,ms));常量delayedSquare=(num)=>睡眠(100)。然后(()=>num*num);const testForEach=(numbersArray)=>{常量存储=[];//此处将此代码视为同步代码numbersArray.forEach(异步(num)=>{const squaredNum=等待延迟平方(num);//这将控制相应的squaredNum值console.log(平方数);store.push(平方数);});//您希望存储阵列已填充,但未填充//这将返回[]console.log(“存储”,存储);};testForEach(数组);//注意,当您测试时,将记录第一个“store[]”//然后squaredNum的内部forEach将记录
解决方案是使用for of循环。
for (const file of files){
const contents = await fs.readFile(file, 'utf8')
}
当然,代码确实有效,但我很确定它并没有达到您期望的效果。它只是触发多个异步调用,但printFiles函数在这之后会立即返回。
按顺序读取
如果您想按顺序读取文件,则实际上不能使用forEach。只需使用现代for…of循环,其中await将按预期工作:
async function printFiles () {
const files = await getFilePaths();
for (const file of files) {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
}
}
并行读取
如果要并行读取文件,则不能使用forEach。每一个异步回调函数调用都会返回一个promise,但您要丢弃它们而不是等待它们。只需使用map,您就可以等待Promise.all提供的一系列承诺:
async function printFiles () {
const files = await getFilePaths();
await Promise.all(files.map(async (file) => {
const contents = await fs.readFile(file, 'utf8')
console.log(contents)
}));
}
只是在原有答案的基础上
原始答案中的平行阅读语法有时令人困惑且难以理解,也许我们可以用不同的方法来编写
async function printFiles() {
const files = await getFilePaths();
const fileReadPromises = [];
const readAndLogFile = async filePath => {
const contents = await fs.readFile(file, "utf8");
console.log(contents);
return contents;
};
files.forEach(file => {
fileReadPromises.push(readAndLogFile(file));
});
await Promise.all(fileReadPromises);
}
用于顺序操作,而不仅仅用于。。。的,循环的正常值也将起作用
async function printFiles() {
const files = await getFilePaths();
for (let i = 0; i < files.length; i++) {
const file = files[i];
const contents = await fs.readFile(file, "utf8");
console.log(contents);
}
}
当fs基于承诺时,Bergi的解决方案非常有效。您可以使用bluebird、fs extra或fs promise。
然而,节点的本地fs库的解决方案如下:
const result = await Promise.all(filePaths
.map( async filePath => {
const fileContents = await getAssetFromCache(filePath, async function() {
// 1. Wrap with Promise
// 2. Return the result of the Promise
return await new Promise((res, rej) => {
fs.readFile(filePath, 'utf8', function(err, data) {
if (data) {
res(data);
}
});
});
});
return fileContents;
}));
注:require('fs')强制将函数作为第三个参数,否则抛出错误:
TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
一个重要的警告是:等待+等待。。of方法和forEach+异步方式实际上有不同的效果。
在真正的for循环中等待将确保所有异步调用都逐一执行。forEach+异步方式将同时发出所有的承诺,这会更快,但有时会不堪重负(如果您执行一些DB查询或访问一些具有容量限制的web服务,并且不想一次发出100000个呼叫)。
如果您不使用async/await,并且希望确保一个接一个地读取文件,那么也可以使用reduce+promise(不太优雅)。
files.reduce((lastPromise, file) =>
lastPromise.then(() =>
fs.readFile(file, 'utf8')
), Promise.resolve()
)
或者您可以创建一个forEachAsync来帮助,但基本上使用相同的for循环底层。
Array.prototype.forEachAsync = async function(cb){
for(let x of this){
await cb(x);
}
}