在 forEach
循环中使用 async
/await
是否有任何问题?我正在尝试遍历文件数组并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
方法是这里的高阶函数
当然代码确实可以工作,但我很确定它没有按照您的预期做。它只是触发多个异步调用,但 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
。每个 async
回调函数调用都返回一个 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)
}));
}
使用 ES2018,您可以大大简化上述所有答案:
async function printFiles () {
const files = await getFilePaths()
for await (const contents of files.map(file => fs.readFile(file, 'utf8'))) {
console.log(contents)
}
}
简化:
for await (const results of array) {
await longRunningTask()
}
console.log('I will wait')
2018-09-10:这个答案最近引起了很多关注,有关异步迭代的更多信息,请参阅Axel Rauschmayer's blog post。
for-await-of
具有同步迭代(在我们的例子中是一个数组)不包括在每次迭代中使用异步操作同时迭代一个数组的情况。如果我没记错的话,将 for-await-of
与非承诺值的同步迭代一起使用与使用普通的 for-of
相同。
files
数组委托给 fs.readFile
?它来自可迭代?
files.map()
返回一个 Promise 数组,不是异步迭代器,for await
就是为此而生的! It will cause unhandled-rejection crashes!
我没有将 Promise.all
与 Array.prototype.map
结合使用(它不能保证 Promise
的解析顺序),而是使用 Array.prototype.reduce
,从已解析的 Promise
开始:
async function printFiles () {
const files = await getFilePaths();
await files.reduce(async (promise, file) => {
// This line will wait for the last async function to finish.
// The first iteration uses an already resolved Promise
// so, it will immediately continue.
await promise;
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
}, Promise.resolve());
}
Promise.resolve()
和 await promise;
发生了什么吗?
Promise.resolve()
返回一个已解析的 Promise
对象,因此 reduce
有一个 Promise
开始。 await promise;
将等待链中的最后一个 Promise
解决。 @GollyJer 文件将按顺序处理,一次一个。
Promise.all
获得大量支持的所提供解决方案之一。示例:Promise.all(files.map(async (file) => { /* code */ }));
npm 上的 p-iteration 模块实现了数组迭代方法,因此可以非常直接地与 async/await 一起使用它们。
以您的情况为例:
const { forEach } = require('p-iteration');
const fs = require('fs-promise');
(async function printFiles () {
const files = await getFilePaths();
await forEach(files, async (file) => {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
});
})();
以下是一些 forEachAsync
原型。请注意,您需要 await
它们:
Array.prototype.forEachAsync = async function (fn) {
for (let t of this) { await fn(t) }
}
Array.prototype.forEachAsyncParallel = async function (fn) {
await Promise.all(this.map(fn));
}
请注意,虽然您可以将其包含在您自己的代码中,但您不应将其包含在您分发给其他人的库中(以避免污染他们的全局变量)。
fn
不是问题吗?如果给定的输入是同步函数怎么办? stackoverflow.com/a/53113299/18387350
价值 1000 字的图片 - 仅适用于顺序方法
背景:我昨晚也遇到了类似的情况。我使用 async 函数作为 foreach 参数。结果是无法预料的。当我对我的代码进行 3 次测试时,它运行了 2 次没有问题并且失败了 1 次。 (有点奇怪)
最后我明白了,做了一些便笺簿测试。
场景 1 - 在 foreach 中使用 async 可以获得多么不连续
https://i.stack.imgur.com/VGXEa.png
const getPromise = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Promise resolved for ${time}s`)
}, time)
})
}
const main = async () => {
const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
console.log('Before For Each Loop')
myPromiseArray.forEach(async (element, index) => {
let result = await element;
console.log(result);
})
console.log('After For Each Loop')
}
main();
场景 2 - 使用 for - of 循环,如上面建议的@Bergi
https://i.stack.imgur.com/QWbRI.png
const getPromise = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Promise resolved for ${time}s`)
}, time)
})
}
const main = async () => {
const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
console.log('Before For Each Loop')
// AVOID USING THIS
// myPromiseArray.forEach(async (element, index) => {
// let result = await element;
// console.log(result);
// })
// This works well
for (const element of myPromiseArray) {
let result = await element;
console.log(result)
}
console.log('After For Each Loop')
}
main();
如果你像我一样是老派,你可以简单地使用经典的 for 循环,它也可以:)
const getPromise = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Promise resolved for ${time}s`)
}, time)
})
}
const main = async () => {
const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
console.log('Before For Each Loop')
// AVOID USING THIS
// myPromiseArray.forEach(async (element, index) => {
// let result = await element;
// console.log(result);
// })
// This works well too - the classic for loop :)
for (let i = 0; i < myPromiseArray.length; i++) {
const result = await myPromiseArray[i];
console.log(result);
}
console.log('After For Each Loop')
}
main();
我希望这对某人有帮助,美好的一天,干杯!
length
以加快 for 循环并防止在每次迭代之间重新计算。
@Bergi 已经给出了如何正确处理这种特殊情况的答案。我不会在这里复制。
在涉及 async
和 await
时,我想解决使用 forEach
和 for
循环之间的区别
forEach
的工作原理
让我们看看 forEach
是如何工作的。根据 ECMAScript Specification,MDN 提供了一个可以用作 polyfill 的 implementation。我将其复制并粘贴到此处并删除评论。
Array.prototype.forEach = function (callback, thisArg) {
if (this == null) { throw new TypeError('Array.prototype.forEach called on null or undefined'); }
var T, k;
var O = Object(this);
var len = O.length >>> 0;
if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function'); }
if (arguments.length > 1) { T = thisArg; }
k = 0;
while (k < len) {
var kValue;
if (k in O) {
kValue = O[k];
callback.call(T, kValue, k, O); // pay attention to this line
}
k++;
}
};
让我们回到您的代码,让我们将回调提取为一个函数。
async function callback(file){
const contents = await fs.readFile(file, 'utf8')
console.log(contents)
}
因此,基本上 callback
返回一个承诺,因为它是用 async
声明的。在 forEach
内部,callback
只是以正常方式调用,如果回调本身返回一个 Promise,则 javascript 引擎不会等待它被解析或拒绝。相反,它将 promise
放入作业队列,并继续执行循环。
callback
中的 await fs.readFile(file, 'utf8')
怎么样?
基本上,当您的 async callback
有机会执行时,js 引擎将暂停直到 fs.readFile(file, 'utf8')
被解析或拒绝,并在完成后恢复执行 async 函数。因此,contents
变量存储来自 fs.readFile
的实际结果,而不是 promise
。因此,console.log(contents)
注销文件内容而不是 Promise
为什么 for ... of
有效?
当我们编写一个通用的 for of
循环时,我们获得了比 forEach
更多的控制权。让我们重构 printFiles
。
async function printFiles () {
const files = await getFilePaths() // Assume this works fine
for (const file of files) {
const contents = await fs.readFile(file, 'utf8')
console.log(contents)
// or await callback(file)
}
}
当评估 for
循环时,我们在 async
函数中有 await
承诺,执行将暂停,直到 await
承诺完成。因此,您可以认为文件是按照确定的顺序逐一读取的。
依次执行
有时,我们确实需要按顺序执行异步函数。例如,我有一些新记录存储在要保存到数据库的数组中,我希望它们按顺序保存,这意味着数组中的第一条记录应该首先保存,然后是第二条,直到最后一条记录被保存。
这是一个例子:
常量记录 = [1, 2, 3, 4];异步函数 saveRecord(record) { return new Promise((resolved, denied) => { setTimeout(()=> { resolved(`record ${record} saved`) }, Math.random() * 500) }); } async function forEachSaveRecords(records) { records.forEach(async (record) => { const res = await saveRecord(record); console.log(res); }) } async function forofSaveRecords(records) { for (const record of记录) { const res = 等待 saveRecord(record);控制台.log(res); } } (async () => { console.log("=== for of save records ===") await forofSaveRecords(records) console.log("=== forEach save records ===") await forEachSaveRecords(记录) })()
我使用 setTimeout
来模拟将记录保存到数据库的过程 - 它是异步的并且花费随机时间。使用 forEach
,记录以不确定的顺序保存,但使用 for..of
,它们按顺序保存。
foreach
不以异步方式处理回调,因此无需等待。
该解决方案还针对内存进行了优化,因此您可以在 10,000 个数据项和请求上运行它。这里的一些其他解决方案会使服务器在大型数据集上崩溃。
在打字稿中:
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 ...
})
files.forEach(async (file) => { const contents = await fs.readFile(file, 'utf8') })
问题是,迭代函数返回的承诺被 forEach()
忽略。因此,所有 fs.readFile
函数都在同一轮事件循环中调用,这意味着它们是并行启动的,而不是顺序启动的,并且在调用 forEach() 后立即继续执行,而无需等待所有 { 2}操作完成。由于 forEach 不等待每个 Promise 解决,因此循环实际上在 Promise 解决之前完成迭代。您最终可能会尝试访问尚不可用的值。
除了 @Bergi’s answer,我想提供第三种选择。它与@Bergi 的第二个示例非常相似,但不是单独等待每个 readFile
,而是创建一个 promise 数组,每个都在最后等待。
import fs from 'fs-promise';
async function printFiles () {
const files = await getFilePaths();
const promises = files.map((file) => fs.readFile(file, 'utf8'))
const contents = await Promise.all(promises)
contents.forEach(console.log);
}
请注意,传递给 .map()
的函数不必是 async
,因为 fs.readFile
无论如何都会返回一个 Promise 对象。因此 promises
是一个 Promise 对象数组,可以发送到 Promise.all()
。
在@Bergi 的回答中,控制台可能会按照读取的顺序记录文件内容。例如,如果一个非常小的文件在一个非常大的文件之前完成读取,它将首先被记录,即使小文件在 files
数组中的大文件之后 也是如此。但是,在我上面的方法中,您可以保证控制台将以与提供的数组相同的顺序记录文件。
替换不起作用的 forEach()
等待循环的简单直接解决方案是将 forEach
替换为 map
并将 Promise.all(
添加到开头。
例如:
await y.forEach(async (x) => {
至
await Promise.all(y.map(async (x) => {
最后需要一个额外的 )
。
在一个文件中弹出几个方法非常容易,这些方法将按序列化顺序处理异步数据并为您的代码提供更传统的风格。例如:
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));
}
当 fs
基于 Promise 时,Bergi's solution 工作得很好。为此,您可以使用 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
从循环中调用异步方法是不好的。这是因为每次循环迭代都会延迟到整个异步操作完成。这不是很高效。它还避免了 async
/await
的并行化优势。
更好的解决方案是一次创建所有 Promise,然后使用 Promise.all()
访问结果。否则,每个后续操作将在前一个操作完成之前不会开始。
因此,代码可以重构如下;
const printFiles = async () => {
const files = await getFilePaths();
const results = [];
files.forEach((file) => {
results.push(fs.readFile(file, 'utf8'));
});
const contents = await Promise.all(results);
console.log(contents);
}
await
实际上首先使它们成为可能。此外,它们并没有“避免异步执行的好处”,因为您仍然可以一次运行多个这样的循环(例如,对 printFiles
的两个并发调用)。
一个重要的警告是:await + for .. of
方法和 forEach + async
方式实际上具有不同的效果。
在真正的 for
循环中包含 await
将确保所有异步调用都被一一执行。而 forEach + async
方式会同时触发所有的 Promise,速度更快但有时会不堪重负(如果您执行一些数据库查询或访问一些有容量限制的 Web 服务并且不想触发一次 100,000 个呼叫)。
如果您不使用 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
相同的迭代 - 访问索引而不是依赖可迭代性 - 并将索引传递给回调。
Array.prototype.reduce
。我在回答中展示了一个示例:stackoverflow.com/a/49499491/2537258
上述两种解决方案都有效,然而,Antonio 用更少的代码完成了这项工作,这就是它如何帮助我从我的数据库中解析数据,从几个不同的子 refs 中解析数据,然后将它们全部推入一个数组并在一个承诺中解决它毕竟是完毕:
Promise.all(PacksList.map((pack)=>{
return fireBaseRef.child(pack.folderPath).once('value',(snap)=>{
snap.forEach( childSnap => {
const file = childSnap.val()
file.id = childSnap.key;
allItems.push( file )
})
})
})).then(()=>store.dispatch( actions.allMockupItems(allItems)))
只是添加到原来的答案
原始答案中的并行阅读语法有时令人困惑且难以阅读,也许我们可以用不同的方法编写它
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);
}
对于顺序操作,不只是 for...of,正常的 for 循环也可以工作
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);
}
}
就像@Bergi 的回应一样,但有一点不同。
如果一个被拒绝,Promise.all
会拒绝所有的承诺。
所以,使用递归。
const readFilesQueue = async (files, index = 0) {
const contents = await fs.readFile(files[index], 'utf8')
console.log(contents)
return files.length <= index
? readFilesQueue(files, ++index)
: files
}
const printFiles async = () => {
const files = await getFilePaths();
const printContents = await readFilesQueue(files)
return printContents
}
printFiles()
附言
readFilesQueue
在 printFiles
之外导致 console.log
引入的副作用*,最好模拟、测试和/或窥探,所以有一个返回内容的函数(旁注)并不酷。
因此,代码可以简单地设计为:三个独立的“纯”函数**并且没有引入副作用,处理整个列表,并且可以轻松修改以处理失败的情况。
const files = await getFilesPath()
const printFile = async (file) => {
const content = await fs.readFile(file, 'utf8')
console.log(content)
}
const readFiles = async = (files, index = 0) => {
await printFile(files[index])
return files.lengh <= index
? readFiles(files, ++index)
: files
}
readFiles(files)
未来编辑/当前状态
Node 支持顶级等待(它还没有插件,不会有并且可以通过和谐标志启用),它很酷但不能解决一个问题(从战略上讲,我只在 LTS 版本上工作)。如何获取文件?
使用组合。给定代码,让我感觉这是在模块内部,因此应该有一个函数来执行此操作。如果没有,您应该使用 IIFE 将角色代码包装到一个异步函数中,创建一个可以为您完成所有工作的简单模块,或者您可以采用正确的方式,即组合。
// more complex version with IIFE to a single module
(async (files) => readFiles(await files())(getFilesPath)
请注意,变量的名称会因语义而改变。您传递一个仿函数(一个可以被另一个函数调用的函数)并接收一个内存指针,该指针包含应用程序的初始逻辑块。
但是,如果不是模块,您需要导出逻辑吗?
将函数包装在异步函数中。
export const readFilesQueue = async () => {
// ... to code goes here
}
或者更改变量的名称,无论如何...
*
副作用是指应用程序的任何隐藏效应,它可以改变状态/行为或在应用程序中引入错误,例如 IO。
**
“纯”,它是撇号,因为它不是纯函数,并且代码可以收敛到纯版本,当没有控制台输出,只有数据操作时。
除此之外,为了纯粹,您需要使用处理副作用的 monad,它们容易出错,并将该错误与应用程序分开处理。
今天我遇到了多种解决方案。在 forEach 循环中运行异步等待函数。通过构建包装器,我们可以做到这一点。
可以通过多种方式完成,如下所示,
方法1:使用包装器。
await (()=>{
return new Promise((resolve,reject)=>{
items.forEach(async (item,index)=>{
try{
await someAPICall();
} catch(e) {
console.log(e)
}
count++;
if(index === items.length-1){
resolve('Done')
}
});
});
})();
方法二:和Array.prototype的泛型函数一样使用
Array.prototype.forEachAsync.js
if(!Array.prototype.forEachAsync) {
Array.prototype.forEachAsync = function (fn){
return new Promise((resolve,reject)=>{
this.forEach(async(item,index,array)=>{
await fn(item,index,array);
if(index === array.length-1){
resolve('done');
}
})
});
};
}
用法 :
require('./Array.prototype.forEachAsync');
let count = 0;
let hello = async (items) => {
// Method 1 - Using the Array.prototype.forEach
await items.forEachAsync(async () => {
try{
await someAPICall();
} catch(e) {
console.log(e)
}
count++;
});
console.log("count = " + count);
}
someAPICall = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("done") // or reject('error')
}, 100);
})
}
hello(['', '', '', '']); // hello([]) empty array is also be handled by default
方法3:
使用 Promise.all
await Promise.all(items.map(async (item) => {
await someAPICall();
count++;
}));
console.log("count = " + count);
方法 4:传统的 for 循环或现代的 for 循环
// Method 4 - using for loop directly
// 1. Using the modern for(.. in..) loop
for(item in items){
await someAPICall();
count++;
}
//2. Using the traditional for loop
for(let i=0;i<items.length;i++){
await someAPICall();
count++;
}
console.log("count = " + count);
Promise.all
的错误实现 - 它们没有考虑到许多边缘情况中的任何一个。
Promise.all
。
Promise.all
不可能但 async
/await
是不可能的条件。不,forEach
绝对不会处理任何承诺错误。
您可以使用 Array.prototype.forEach
,但 async/await 不是那么兼容。这是因为从异步回调返回的承诺预计会得到解决,但 Array.prototype.forEach
不会通过执行其回调来解决任何承诺。因此,您可以使用 forEach,但您必须自己处理承诺解决方案。
这是一种使用 Array.prototype.forEach
依次读取和打印每个文件的方法
async function printFilesInSeries () {
const files = await getFilePaths()
let promiseChain = Promise.resolve()
files.forEach((file) => {
promiseChain = promiseChain.then(() => {
fs.readFile(file, 'utf8').then((contents) => {
console.log(contents)
})
})
})
await promiseChain
}
这是一种并行打印文件内容的方法(仍然使用 Array.prototype.forEach
)
async function printFilesInParallel () {
const files = await getFilePaths()
const promises = []
files.forEach((file) => {
promises.push(
fs.readFile(file, 'utf8').then((contents) => {
console.log(contents)
})
)
})
await Promise.all(promises)
}
目前 Array.forEach 原型属性不支持异步操作,但我们可以创建自己的 poly-fill 来满足我们的需求。
// 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}
就是这样!您现在可以在这些操作之后定义的任何数组上使用 async forEach 方法。
让我们测试一下...
// 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
... 等等 :)
需要注意的一些事项:
您的 iteratorFunction 必须是异步函数或承诺
在 Array.prototype.
要查看如何出错,请在方法末尾打印 console.log。
一般可能出错的事情:
任意顺序。
printFiles 可以在打印文件之前完成运行。
表现不佳。
这些并不总是错误的,但经常出现在标准用例中。
通常,使用 forEach 将导致除最后一个之外的所有结果。它会在不等待函数的情况下调用每个函数,这意味着它告诉所有函数开始然后完成而不等待函数完成。
import fs from 'fs-promise'
async function printFiles () {
const files = (await getFilePaths()).map(file => fs.readFile(file, 'utf8'))
for(const file of files)
console.log(await file)
}
printFiles()
这是本机 JS 中的一个示例,它将保持顺序,防止函数过早返回并在理论上保持最佳性能。
这将:
启动所有文件读取以并行发生。
通过使用 map 将文件名映射到等待的承诺来保留顺序。
按照数组定义的顺序等待每个承诺。
使用此解决方案,第一个文件将在可用时立即显示,而无需先等待其他文件可用。
它还将同时加载所有文件,而不必等待第一个文件完成才能开始读取第二个文件。
此版本和原始版本的唯一缺点是,如果一次启动多个读取,则由于一次可能发生更多错误,因此处理错误会更加困难。
对于一次读取文件的版本,然后将在失败时停止,而不会浪费时间尝试读取更多文件。即使有一个精心设计的取消系统,也很难避免它在第一个文件上失败,但也已经读取了大多数其他文件。
性能并不总是可预测的。虽然许多系统使用并行文件读取会更快,但有些系统更喜欢顺序读取。有些是动态的,可能会在负载下发生变化,提供延迟的优化并不总是在激烈的争用下产生良好的吞吐量。
该示例中也没有错误处理。如果某些事情要求它们要么全部成功显示,要么根本不显示,它不会那样做。
建议在每个阶段使用 console.log 和假文件读取解决方案(改为随机延迟)进行深入实验。尽管许多解决方案在简单的情况下似乎都做同样的事情,但它们都有细微的差异,需要一些额外的审查才能消除。
使用这个模拟来帮助区分解决方案:
(async () => {
const start = +new Date();
const mock = () => {
return {
fs: {readFile: file => new Promise((resolve, reject) => {
// Instead of this just make three files and try each timing arrangement.
// IE, all same, [100, 200, 300], [300, 200, 100], [100, 300, 200], etc.
const time = Math.round(100 + Math.random() * 4900);
console.log(`Read of ${file} started at ${new Date() - start} and will take ${time}ms.`)
setTimeout(() => {
// Bonus material here if random reject instead.
console.log(`Read of ${file} finished, resolving promise at ${new Date() - start}.`);
resolve(file);
}, time);
})},
console: {log: file => console.log(`Console Log of ${file} finished at ${new Date() - start}.`)},
getFilePaths: () => ['A', 'B', 'C', 'D', 'E']
};
};
const printFiles = (({fs, console, getFilePaths}) => {
return async function() {
const files = (await getFilePaths()).map(file => fs.readFile(file, 'utf8'));
for(const file of files)
console.log(await file);
};
})(mock());
console.log(`Running at ${new Date() - start}`);
await printFiles();
console.log(`Finished running at ${new Date() - start}`);
})();
使用 Task、futurize 和一个可遍历的 List,你可以简单地做
async function printFiles() {
const files = await getFiles();
List(files).traverse( Task.of, f => readFile( f, 'utf-8'))
.fork( console.error, console.log)
}
这是您的设置方式
import fs from 'fs';
import { futurize } from 'futurize';
import Task from 'data.task';
import { List } from 'immutable-ext';
const future = futurizeP(Task)
const readFile = future(fs.readFile)
构建所需代码的另一种方法是
const printFiles = files =>
List(files).traverse( Task.of, fn => readFile( fn, 'utf-8'))
.fork( console.error, console.log)
或者甚至更注重功能
// 90% of encodings are utf-8, making that use case super easy is prudent
// handy-library.js
export const readFile = f =>
future(fs.readFile)( f, 'utf-8' )
export const arrayToTaskList = list => taskFn =>
List(files).traverse( Task.of, taskFn )
export const readFiles = files =>
arrayToTaskList( files, readFile )
export const printFiles = files =>
readFiles(files).fork( console.error, console.log)
然后从父函数
async function main() {
/* awesome code with side-effects before */
printFiles( await getFiles() );
/* awesome code with side-effects after */
}
如果您真的希望编码更灵活,您可以这样做(为了好玩,我正在使用建议的 Pipe Forward operator )
import { curry, flip } from 'ramda'
export const readFile = fs.readFile
|> future,
|> curry,
|> flip
export const readFileUtf8 = readFile('utf-8')
PS - 我没有在控制台上尝试这个代码,可能有一些错别字......“直自由泳,从圆顶顶部!”正如 90 后的孩子所说。 :-p
这是在 forEach 循环中使用异步的一个很好的例子。
编写自己的 asyncForEach
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array)
}
}
你可以像这样使用它
await asyncForEach(array, async function(item,index,array){
//await here
}
)
OP的原始问题
在 forEach 循环中使用 async/await 有什么问题吗? ...
在@Bergi 的selected answer 中进行了一定程度的介绍,它展示了如何串行和并行处理。然而,并行性还有其他问题 -
订单——@chharvey 指出——
例如,如果一个非常小的文件在一个非常大的文件之前完成读取,它将首先被记录,即使文件数组中小文件在大文件之后也是如此。
可能一次打开太多文件——Bergi 在另一个答案下的评论
一次打开数千个文件以同时读取它们也不好。人们总是需要评估顺序、并行或混合方法是否更好。
因此,让我们解决这些问题,展示简洁明了的实际代码,并且不使用第三方库。可以轻松剪切、粘贴和修改的东西。
并行读取(一次全部),串行打印(每个文件尽可能早)。
最简单的改进是像 @Bergi's answer 中那样执行完全并行,但要进行一些小的更改,以便在保持顺序的同时尽快打印每个文件。
async function printFiles2() {
const readProms = (await getFilePaths()).map((file) =>
fs.readFile(file, "utf8")
);
await Promise.all([
await Promise.all(readProms), // branch 1
(async () => { // branch 2
for (const p of readProms) console.log(await p);
})(),
]);
}
上面,两个单独的分支同时运行。
分支 1:一次并行读取,
分支 2:串行读取以强制排序,但无需等待
那很简单。
在并发限制的情况下并行读取,串行打印(每个文件尽可能早)。
“并发限制”意味着同时读取的文件不超过 N
个。
就像一家商店一次只允许这么多客户(至少在 COVID 期间)。
首先介绍一个辅助函数——
function bootablePromise(kickMe: () => Promise<any>) {
let resolve: (value: unknown) => void = () => {};
const promise = new Promise((res) => { resolve = res; });
const boot = () => { resolve(kickMe()); };
return { promise, boot };
}
函数 bootablePromise(kickMe:() => Promise<any>)
将函数 kickMe
作为参数来启动任务(在我们的例子中是 readFile
)。但它不会立即启动。
bootablePromise
返回几个属性
Promise 类型的 Promise
启动类型函数 ()=>void
promise
人生有两个阶段
承诺开始一项任务 承诺完成一项它已经开始的任务。
调用 boot()
时,promise
从第一个状态转换到第二个状态。
bootablePromise
用于 printFiles
--
async function printFiles4() {
const files = await getFilePaths();
const boots: (() => void)[] = [];
const set: Set<Promise<{ pidx: number }>> = new Set<Promise<any>>();
const bootableProms = files.map((file,pidx) => {
const { promise, boot } = bootablePromise(() => fs.readFile(file, "utf8"));
boots.push(boot);
set.add(promise.then(() => ({ pidx })));
return promise;
});
const concurLimit = 2;
await Promise.all([
(async () => { // branch 1
let idx = 0;
boots.slice(0, concurLimit).forEach((b) => { b(); idx++; });
while (idx<boots.length) {
const { pidx } = await Promise.race([...set]);
set.delete([...set][pidx]);
boots[idx++]();
}
})(),
(async () => { // branch 2
for (const p of bootableProms) console.log(await p);
})(),
]);
}
和以前一样有两个分支
分支 1:用于运行和处理并发性。
分支 2:用于打印
现在的区别是允许同时运行的承诺不超过 concurLimit
个。
重要的变量是
boots:要调用的函数数组以强制其相应的转换承诺。它仅用于分支 1。
set:随机访问容器中有承诺,因此一旦履行,它们就可以很容易地删除。此 contianer 仅在分支 1 中使用。
bootableProms:这些是最初在集合中的 smae 承诺,但它是一个数组而不是一个集合,并且该数组永远不会改变。它仅用于分支 2。
使用模拟 fs.readFile
运行,所需时间如下(文件名与时间,以毫秒为单位)。
const timeTable = {
"1": 600,
"2": 500,
"3": 400,
"4": 300,
"5": 200,
"6": 100,
};
可以看到像这样的测试运行时间,表明并发正在工作——
[1]0--0.601
[2]0--0.502
[3]0.503--0.904
[4]0.608--0.908
[5]0.905--1.105
[6]0.905--1.005
在 typescript playground sandbox 中作为可执行文件提供
正如其他答案所提到的,您可能希望它按顺序而不是并行执行。 IE。运行第一个文件,等到它完成,然后一旦它完成运行第二个文件。那不是会发生的。
我认为重要的是要解决为什么不会发生这种情况。
想想 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
?
不,不会的。令人困惑的是,这不是 await
的工作方式。从 the docs:
等待拆分执行流程,允许异步函数的调用者恢复执行。在 await 推迟了 async 函数的继续执行之后,随后的语句就会执行。如果此 await 是其函数执行的最后一个表达式,则继续通过向函数的调用者返回待处理的 Promise 以完成 await 的函数并恢复该调用者的执行。
因此,如果您有以下情况,则不会在 "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
做了一些 await
而停止,forEach
也不会因为 logFile
做了一些 await
而停止。
与 Antonio Val 的 p-iteration
类似,另一个 npm 模块是 async-af
:
const AsyncAF = require('async-af');
const fs = require('fs-promise');
function printFiles() {
// since AsyncAF accepts promises or non-promises, there's no need to await here
const files = getFilePaths();
AsyncAF(files).forEach(async file => {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
});
}
printFiles();
或者,async-af
有一个记录承诺结果的静态方法 (log/logAF):
const AsyncAF = require('async-af');
const fs = require('fs-promise');
function printFiles() {
const files = getFilePaths();
AsyncAF(files).forEach(file => {
AsyncAF.log(fs.readFile(file, 'utf8'));
});
}
printFiles();
但是,该库的主要优点是您可以链接异步方法来执行以下操作:
const aaf = require('async-af');
const fs = require('fs-promise');
const printFiles = () => aaf(getFilePaths())
.map(file => fs.readFile(file, 'utf8'))
.forEach(file => aaf.log(file));
printFiles();
在 2022 年,我仍然建议使用外部库来处理所有这些异步流程。我已经为类似的事情创建了模块 alot🔗。
你的例子是:
import fs from 'fs-promise'
import alot from 'alot'
async function printFiles () {
const files = await getFilePaths() // Assume this works fine
await alot(files)
.forEachAsync(async file => {
let content = await fs.readFile(file, 'utf8');
console.log(content);
})
.toArrayAsync({ threads: 4 });
}
}
printFiles()
对于简单的示例,异步 for..of
肯定会完成这项工作,但是一旦任务变得更加复杂,您就必须为此使用一些实用程序。
Alot 有许多其他方法可以链接,例如 mapAsync
、filterAsync
、groupAsync
等。
举个例子:
加载带有产品元数据的 JSON 文件
提取产品ID
从服务器加载产品
过滤价格 > 100 美元的商品
按价格升序排列
进入前 50 名
import fs from 'fs-promise'
import alot from 'alot'
import axios from 'axios'
import { File } from 'atma-io'
let paths = await getFilePaths();
let products = await alot(paths)
.mapAsync(async path => await File.readAsync<IProductMeta>(path))
.mapAsync(async meta => await axios.get(`${server}/api/product/${meta.productId}`))
.mapAsync(resp => resp.data)
.filterAsync(product => product.price > 100)
.sortBy(product => product.price, 'asc')
.takeAsync(50)
.toArrayAsync({ threads: 5, errors: 'include' });
threads: 4
是什么? JS没有线程
async\await
故事都意味着事件循环一直等到它返回结果。通过定义 threads
,我们设置了启动并行任务的数量,其他任务将等待至少一个任务(fs、网络、工作人员等)准备好。
如果您想同时迭代所有元素:
async function asyncForEach(arr, fn) {
await Promise.all(arr.map(fn));
}
如果您想非并发地迭代所有元素(例如,当您的映射函数有副作用或一次在所有数组元素上运行映射器时资源成本太高):
选项 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);
}
}
Promise
constructor antipattern。
如果你不能使用 async/await(IE11、旧打包器等)那么你可以试试这个递归函数。我使用 fetch
作为异步调用,但您可以使用任何返回承诺的函数。
var urlsToGet = ['https://google.com', 'https://yahoo.com'];
fetchOneAtATime(urlsToGet);
function fetchOneAtATime(urls) {
if (urls.length === 0) {
return;
}
fetch(urls[0]).finally(() => fetchOneAtATime(urls.slice(1)));
}
.shift()
之前检查 urls.length
,最好使用 urls[0]
和 urls.slice(1)
而不是清空传递给函数的数组。
finally
而不是 then
?与 async
/await
不同,这将忽略错误
for ... of ...
有效?async
/await
转换为生成器函数,使用forEach
意味着每次迭代都有一个单独的生成器函数,与其他迭代无关。因此它们将独立执行,并且与其他人没有next()
的上下文。实际上,一个简单的for()
循环也可以工作,因为迭代也在一个生成器函数中。await
暂停当前的 function 评估,包括所有控制结构。是的,在这方面它与生成器非常相似(这就是它们用于填充 async/await 的原因)。async
函数与Promise
执行器回调完全不同,但是map
回调在这两种情况下都会返回一个承诺。for…of
将与forEach
一样工作。不,我的意思是要强调现代 JS 代码中没有.forEach
的位置。