在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,所以我只想问问这是否有问题。
目前,Array.forEach原型属性不支持异步操作,但我们可以创建自己的多边形填充来满足我们的需要。
// Example of asyncForEach Array poly-fill for NodeJs
// file: asyncForEach.js
// Define asynForEach function
async function asyncForEach(iteratorFunction){
let indexer = 0
for(let data of this){
await iteratorFunction(data, indexer)
indexer++
}
}
// Append it as an Array prototype property
Array.prototype.asyncForEach = asyncForEach
module.exports = {Array}
就这样!现在,在这些操作之后定义的任何数组上都可以使用asyncforEach方法。
让我们测试一下。。。
// Nodejs style
// file: someOtherFile.js
const readline = require('readline')
Array = require('./asyncForEach').Array
const log = console.log
// Create a stream interface
function createReader(options={prompt: '>'}){
return readline.createInterface({
input: process.stdin
,output: process.stdout
,prompt: options.prompt !== undefined ? options.prompt : '>'
})
}
// Create a cli stream reader
async function getUserIn(question, options={prompt:'>'}){
log(question)
let reader = createReader(options)
return new Promise((res)=>{
reader.on('line', (answer)=>{
process.stdout.cursorTo(0, 0)
process.stdout.clearScreenDown()
reader.close()
res(answer)
})
})
}
let questions = [
`What's your name`
,`What's your favorite programming language`
,`What's your favorite async function`
]
let responses = {}
async function getResponses(){
// Notice we have to prepend await before calling the async Array function
// in order for it to function as expected
await questions.asyncForEach(async function(question, index){
let answer = await getUserIn(question)
responses[question] = answer
})
}
async function main(){
await getResponses()
log(responses)
}
main()
// Should prompt user for an answer to each question and then
// log each question and answer as an object to the terminal
我们可以对其他一些数组函数(如map。。。
async function asyncMap(iteratorFunction){
let newMap = []
let indexer = 0
for(let data of this){
newMap[indexer] = await iteratorFunction(data, indexer, this)
indexer++
}
return newMap
}
Array.prototype.asyncMap = asyncMap
…等等:)
需要注意的一些事项:
迭代器函数必须是异步函数或promise在Array.protocol.<yourAsyncFunc>=<yourAsync Func>之前创建的任何数组都不具有此功能
一个重要的警告是:等待+等待。。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);
}
}
正如其他答案所提到的,您可能希望它按顺序而不是并行执行。即,运行第一个文件,等待完成,然后一旦完成,运行第二个文件。这不会发生。
我认为很重要的是要解决为什么没有发生这种情况。
想想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等待而停止。