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
を使うべきではないと誰かに教えてもらったので、問題があるかどうかを尋ねたかっただけです。
コードは機能することは確かですが、期待どおりに機能しないことは間違いありません。複数の非同期呼び出しを起動するだけですが、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)
}));
}
ES2018を使用すると、上記のすべての回答を非常に簡単にすることができます。
async function printFiles () {
const files = await getFilePaths()
for await (const file of fs.readFile(file, 'utf8')) {
console.log(contents)
}
}
仕様を参照してください: https://github.com/tc39/proposal-async-iteration
2018-09-10:この答えは最近非常に注目されています。非同期反復についてのより詳しい情報はAxel Rauschmayerのブログ投稿をご覧ください: http://2ality.com/2016/10/asynchronous-iteration.html
私にとってPromise.all()
をmap()
と一緒に使うのは理解するのが少し難しくて冗長ですが、普通のJSでやりたいのであればそれがあなたのベストショットです。
モジュールを追加しても構わない場合は、Array反復メソッドを実装したので、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);
});
}
printFiles()
Promise.all
と一緒にArray.prototype.map
の代わりに(これはPromise
が解決される順序を保証するものではありません)、解決されたPromise
から始めてArray.prototype.reduce
を使用します。
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());
}
これがforEach非同期プロトタイプです。
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));
}
@ Bergi's answer に加えて、3番目の選択肢を提供したいと思います。 @Bergiの2番目の例に非常に似ていますが、各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);
}
fs.readFile
はとにかくPromiseオブジェクトを返すため、.map()
に渡される関数はasync
である必要はないことに注意してください。したがって、promises
はPromiseオブジェクトの配列であり、Promise.all()
に送信できます。
@Bergiの回答では、コンソールはファイルの内容を順不同で記録する場合があります。たとえば、本当に小さなファイルが大きなファイルの前に読み終わった場合、小さなファイルがafterに来たとしても、最初にログに記録されますfiles
配列。ただし、上記の方法では、コンソールはファイルが読み取られたときと同じ順序でログを記録することが保証されています。
上記の解決策は両方ともうまくいきませんが、Antonio'sはコードを少なくして仕事をこなします。ここで、いくつかの異なる子参照からデータベースからデータを解決し、それらをすべて配列にプッシュして解決しました完了:
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)))
非同期データを直列化された順序で処理し、コードに従来の風味を与える、ファイル内の2つのメソッドをポップするのは非常に簡単です。例えば:
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));
}
1つの重要な警告:await + for .. of
メソッドとforEach + async
方法の効果は実際には異なります。
await
を実際のfor
ループ内に含めると、すべての非同期呼び出しが1つずつ実行されるようになります。 forEach + async
の方法はすべてのプロミスを同時に起動します。これは高速ですが、時には圧倒されます(DBクエリを実行するか、ボリューム制限のある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);
}
}
現在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}
以上です!これらの操作の後に定義されているすべての配列で、非同期の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
... 等々 :)
注意すべき点がいくつかあります。
Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>
より前に作成された配列では、この機能は利用できません。タスク、未来化、そしてトラバース可能なリストを使うと、簡単にできます。
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)
目的のコードを構造化するもう1つの方法は、
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')
シモンズ - 私はコンソールでこのコードを試していませんでした、いくつかのタイプミスがあるかもしれません... 90年代の子供たちが言うように。 :-p
Bergiの解決策fs
が約束ベースであるときはうまく動作します。これにはbluebird
、fs-extra
、またはfs-promise
を使用できます。
ただし、 nodeのネイティブ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')
は強制的にfunctionを3番目の引数として受け取ります。それ以外の場合はエラーをスローします。
TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
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
にはpromiseの結果を記録する静的メソッド(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();