web-dev-qa-db-ja.com

ForEachループでasync/awaitを使用する

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を使うべきではないと誰かに教えてもらったので、問題があるかどうかを尋ねたかっただけです。

566
saadq

コードは機能することは確かですが、期待どおりに機能しないことは間違いありません。複数の非同期呼び出しを起動するだけですが、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)
  }));
}
1180
Bergi

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

83
Francisco Mateo

私にとって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()

p反復

22
Antonio Val

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());
}
20
Timothy Zorn

これが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));
}
11
Matt

@ 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配列。ただし、上記の方法では、コンソールはファイルが読み取られたときと同じ順序でログを記録することが保証されています。

5
chharvey

上記の解決策は両方ともうまくいきませんが、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
Hooman Askari

非同期データを直列化された順序で処理し、コードに従来の風味を与える、ファイル内の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));
}
2
Jay Edwards

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);
    }
}
2
LeOn - Han Li

現在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

... 等々 :)

注意すべき点がいくつかあります。

  • あなたのiteratorFunctionは非同期関数か約束でなければなりません
  • Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>より前に作成された配列では、この機能は利用できません。
1
Beau

タスク、未来化、そしてトラバース可能なリストを使うと、簡単にできます。

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

1
Babakness

Bergiの解決策fsが約束ベースであるときはうまく動作します。これにはbluebirdfs-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
0

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();

async-af

0
Scott Rudiger