将 async/await 与 forEach 循环结合使用
- 2024-11-02 21:00:00
- admin 原创
- 51
问题描述:
在循环中使用async
/是否存在问题?我正在尝试循环遍历文件数组并遍历每个文件的内容。await
`forEach`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
解决方案 1:
当然,代码确实可以工作,但我确信它不会按照您的预期执行。它只是触发了多个异步调用,但函数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
回调函数调用都会返回一个承诺,但您将其丢弃而不是等待它们。只需使用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)
}));
}
解决方案 2:
使用 ES2018,您可以将上述所有答案大大简化为:
async function printFiles () {
const files = await getFilePaths()
for await (const contents of files.map(file => fs.readFile(file, 'utf8'))) {
console.log(contents)
}
}
参见规范:proposal-async-iteration
简化:
for await (const results of array) {
await longRunningTask()
}
console.log('I will wait')
2018-09-10:这个答案最近引起了很多关注,请参阅Axel Rauschmayer 的博客文章以获取有关异步迭代的更多信息。
解决方案 3:
Promise.all
我没有使用(这不能保证s 的解析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());
}
解决方案 4:
files.forEach(async (file) => { const contents = await fs.readFile(file, 'utf8') })
问题是,迭代函数返回的 promise 被 忽略forEach()
。forEach
不会等待每次异步代码执行完成后再转到下一次迭代。所有函数fs.readFile
将在同一轮事件循环中调用,这意味着它们是并行启动的,而不是按顺序启动的,并且在调用 forEach() 后立即继续执行,而不等待所有操作fs.readFile
完成。由于 forEach 不会等待每个 promise 解决,因此循环实际上在 promise 解决之前就完成了迭代。您期望在forEach
完成后,所有异步代码都已执行,但事实并非如此。您最终可能会尝试访问尚不可用的值。
您可以使用此示例代码测试行为
const array = [1, 2, 3];
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const delayedSquare = (num) => sleep(100).then(() => num * num);
const testForEach = (numbersArray) => {
const store = [];
// this code here treated as sync code
numbersArray.forEach(async (num) => {
const squaredNum = await delayedSquare(num);
// this will console corrent squaredNum value
// console.log(squaredNum) will log after console.log("store",store)
console.log(squaredNum);
store.push(squaredNum);
});
// you expect that store array is populated as [1,4,9] but it is not
// this will return []
console.log("store",store);
};
testForEach(array);
// Notice, when you test, first "store []" will be logged
// then squaredNum's inside forEach will log
运行代码片段Hide results展开片段
解决方案是使用 for-of 循环。
for (const file of files){
const contents = await fs.readFile(file, 'utf8')
}
解决方案 5:
一图胜千言 - 仅适用于顺序方法
背景:昨晚我遇到了类似的情况。我使用异步函数作为 foreach 参数。结果不可预测。当我对我的代码进行 3 次测试时,它运行了 2 次,没有出现问题,1 次失败。(有些奇怪)
最后我终于搞清楚了,并做了一些便笺簿测试。
场景 1 - foreach 中的异步会带来怎样的不连续性
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 所建议的那样
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();
我希望这对某人有帮助,祝你有个愉快的一天,干杯!
解决方案 6:
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);
});
})();
解决方案 7:
以下是一些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));
}
请注意,虽然您可以将其包含在您自己的代码中,但您不应将其包含在分发给他人的库中(以避免污染他们的全局变量)。
使用方法:
await myArray. forEachAsyncParallel( async (item) => { await myAsyncFunction(item) })
请注意,您也需要一个现代的,比如esrun
在 TypeScript 的顶层使用 await。
解决方案 8:
@Bergi 已经给出了如何正确处理这种特殊情况的答案。我就不重复了。
我想谈谈使用forEach
和for
循环之间的区别,当谈到async
和await
如何forEach
运作
我们来看看它是如何forEach
工作的。根据ECMAScript 规范,MDN 提供了一个可以用作 polyfill 的实现。我将其复制并粘贴到这里,并删除了注释。
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
只是以正常方式调用,如果回调本身返回承诺,则 JavaScript 引擎不会等待它被解决或拒绝。相反,它将 放入promise
作业队列中,并继续执行循环。
await fs.readFile(file, 'utf8')
里面怎么样callback
?
基本上,当您的异步callback
有机会执行时,js 引擎将暂停,直到fs.readFile(file, 'utf8')
被解析或拒绝,并在完成后恢复执行异步函数。因此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
循环时,我们在函数await
内部有一个承诺async
,执行将暂停,直到await
承诺得到解决。所以,你可以认为文件是按照确定的顺序一个接一个地读取的。
按顺序执行
有时,我们确实需要按顺序执行异步函数。例如,我有一些新记录存储在数组中以保存到数据库,我希望它们按顺序保存,这意味着数组中的第一个记录应首先保存,然后是第二个,直到最后一个记录被保存。
以下是一个例子:
const records = [1, 2, 3, 4];
async function saveRecord(record) {
return new Promise((resolved, rejected) => {
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 records) {
const res = await saveRecord(record);
console.log(res);
}
}
(async () => {
console.log("=== for of save records ===")
await forofSaveRecords(records)
console.log("=== forEach save records ===")
await forEachSaveRecords(records)
})()
运行代码片段Hide results展开片段
我使用setTimeout
来模拟将记录保存到数据库的过程 - 它是异步的并且花费随机时间。使用forEach
,记录以不确定的顺序保存,但使用for..of
,记录将按顺序保存。
解决方案 9:
此解决方案还针对内存进行了优化,因此您可以在 10,000 个数据项和请求上运行它。此处的其他一些解决方案会在处理大型数据集时导致服务器崩溃。
在 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 ...
})
解决方案 10:
forEach()
替换不起作用的 await 循环的一个简单解决方案是替换forEach
为map
并添加Promise.all(
到开头。
例如:
await y.forEach(async (x) => {
到
await Promise.all(y.map(async (x) => {
)
最后还需要一个额外的东西。
解决方案 11:
除了@Bergi 的回答之外,我还想提供第三种选择。它与 @Bergi 的第二个示例非常相似,但readFile
您不是单独等待每个承诺,而是创建一个承诺数组,最后等待每个承诺。
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
。但是,在我上面的方法中,可以保证控制台将按照提供的数组的顺序记录文件。
解决方案 12:
在一个文件中弹出几个方法,这些方法将以序列化的顺序处理异步数据,并为您的代码提供更传统的风格,这是非常轻松的。例如:
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));
}
解决方案 13:
Bergi 的解决方案在基于承诺的情况下效果很好fs
。您可以使用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
解决方案 14:
async
从循环中调用异步方法并不好。这是因为每次循环迭代都会被延迟,直到整个异步操作完成。这样做性能不佳。它还会抵消/的并行化优势await
。
更好的解决方案是一次性创建所有承诺,然后使用 获取结果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);
}
解决方案 15:
一个重要的警告是:await + for .. of
方法和forEach + async
方式实际上有不同的效果。
await
内部有真正的for
循环将确保所有异步调用都逐一执行。这种forEach + async
方式将同时触发所有承诺,速度更快,但有时会不堪重负(如果您执行某些数据库查询或访问某些具有容量限制的 Web 服务,并且不想一次触发 100,000 个调用)。
reduce + promise
如果您不想使用async/await
并且想确保文件被一个接一个地读取,那么您也可以使用(不太优雅) 。
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);
}
}
解决方案 16:
您可以使用Array.prototype.forEach
,但 async/await 不太兼容。这是因为异步回调返回的 Promise 期望得到解决,但Array.prototype.forEach
不会解决其回调执行中的任何 Promise。因此,您可以使用 forEach,但您必须自己处理 Promise 解决。
以下是使用以下方法连续读取和打印每个文件的方法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)
}
解决方案 17:
只是添加到原始答案
原始答案中的平行阅读语法有时令人困惑且难以阅读,也许我们可以用不同的方法来写
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);
}
}
解决方案 18:
OP 的原始问题
在 forEach 循环中使用 async/await 是否存在问题?...
在@Bergi 的精选答案中有所涉及,其中展示了如何串行和并行处理。然而,并行性还存在其他问题 -
秩序—— @chharvey指出——
例如,如果一个非常小的文件在一个非常大的文件之前完成读取,它将首先被记录,即使小文件在文件数组中位于大文件之后。
可能一次打开太多文件——Bergi 在另一个答案下的评论
同时打开数千个文件并发读取它们也不是一件好事。人们总是要评估是顺序、并行还是混合方法更好。
因此,让我们通过展示简洁明了且不使用第三方库的实际代码来解决这些问题。这些代码易于剪切、粘贴和修改。
并行读取(一次全部读取),串行打印(每个文件尽早打印)。
最简单的改进是像@Bergi 的答案那样执行完全并行,但做一个小小的改变,以便在保持顺序的同时尽快打印每个文件。
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
同时读取的文件数量不会超过 100 个。
就像一家商店一次只允许一定数量的顾客进入(至少在疫情期间)。
首先介绍一个辅助函数 -
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
boot
函数类型()=>void
promise
人生有两个阶段
承诺开始某项任务
作为一个承诺,完成一项它已经开始的任务。
promise
当被调用时从第一状态转换到第二状态boot()
。
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
允许同时运行 100 个 Promises。
重要的变量是
boots
:强制其对应的 Promise 转换的函数数组。它仅在分支 1 中使用。set
:Promises 位于一个随机访问容器中,因此一旦实现,就可以轻松将其移除。此容器仅在分支 1 中使用。bootableProms
:这些 Promises 与 中最初的相同set
,但它是一个数组而不是集合,并且数组永远不会改变。它仅在分支 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 游乐场沙箱中可作为可执行文件使用
解决方案 19:
你可以使用像这样的简单传统 for 循环
for(let i = 0; i< products.length; i++){
await <perform some action like database read>
}
解决方案 20:
上述两种解决方案均可行,但 Antonio 的解决方案使用更少的代码完成了工作,下面是它如何帮助我从数据库、从几个不同的子引用解析数据,然后将它们全部推送到一个数组中,并在完成所有操作后在承诺中解析它:
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)))
解决方案 21:
与@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 支持顶级 await(目前还没有插件,不会有,但可以通过 harmony 标志启用),这很酷,但不能解决一个问题(从策略上讲,我只在 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,这些 monad 容易出错,并将该错误与应用程序分开处理。
解决方案 22:
目前 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.<yourAsyncFunc> = <yourAsyncFunc>
均无法使用此功能
解决方案 23:
今天我遇到了多种解决方案。在 forEach 循环中运行 async await 函数。通过构建包装器,我们可以实现这一点。
此处的链接提供了有关其内部工作原理、本机 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')
}
});
});
})();
方法 2:使用与 Array.prototype 相同的泛型函数
数组.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);
解决方案 24:
要查看出错原因,请在方法末尾打印 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}`);
})();
解决方案 25:
使用 Task、futurize 和可遍历列表,你可以简单地做
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 运算符)
import { curry, flip } from 'ramda'
export const readFile = fs.readFile
|> future,
|> curry,
|> flip
export const readFileUtf8 = readFile('utf-8')
PS - 我没有在控制台上尝试过此代码,可能有一些拼写错误……“直线自由式,从圆顶顶部飞出!”就像 90 年代的孩子们会说的那样。:-p
解决方案 26:
正如其他答案所提到的,您可能希望它按顺序执行,而不是并行执行。即运行第一个文件,等到它完成,然后一旦完成,再运行第二个文件。但事实并非如此。
我认为,搞清楚为什么这种情况不会发生非常重要。
想想它是如何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
循环会等待这个 ,await
然后再继续i++
?
不会。令人困惑的是,它不是这样await
工作的。来自文档:
await 会拆分执行流,允许异步函数的调用者恢复执行。await 推迟异步函数的继续执行后,将执行后续语句。如果此 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
事情而停止。
解决方案 27:
与 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();
async-af
解决方案 28:
这是一个在 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
}
)
解决方案 29:
如果您想同时迭代所有元素:
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/await
async function asyncForEachStrict(arr, fn) {
for (let idx = 0; idx < arr.length; idx += 1) {
const cur = arr[idx];
await fn(cur, idx, arr);
}
}
解决方案 30:
对于 TypeScript 用户,Promise.all(array.map(iterator))
具有工作类型的包装器
Promise.all(array.map(iterator))
由于 TypeScript 的 stdlib 支持已经处理泛型,因此使用具有正确的类型。Promise.all(array.map(iterator))
但是,每次需要异步映射时都进行复制粘贴显然不是最佳做法,并且Promise.all(array.map(iterator))
不能很好地传达代码的意图 - 因此大多数开发人员会将其包装到asyncMap()
包装函数中。但是这样做需要使用泛型来确保设置的值const value = await asyncMap()
具有正确的类型。
export const asyncMap = async <ArrayItemType, IteratorReturnType>(
array: Array<ArrayItemType>,
iterator: (
value: ArrayItemType,
index?: number
) => Promise<IteratorReturnType>
): Promise<Array<IteratorReturnType>> => {
return Promise.all(array.map(iterator));
};
并进行快速测试:
it(`runs 3 items in parallel and returns results`, async () => {
const result = await asyncMap([1, 2, 3], async (item: number) => {
await sleep(item * 100);
return `Finished ${item}`;
});
expect(result.length).toEqual(3);
// Each item takes 100, 200 and 300ms
// So restricting this test to 300ms plus some leeway
}, 320);
sleep()
只是:
const sleep = async (timeInMs: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, timeInMs));
};
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件