Node JS Promise.allおよびforEach


120

非同期メソッドを公開する構造のような配列があります。asyncメソッドを呼び出すと、配列構造が返され、さらに多くのasyncメソッドが公開されます。この構造から取得した値を格納する別のJSONオブジェクトを作成しているので、コールバックでの参照の追跡に注意する必要があります。

私はブルートフォースソリューションをコーディングしましたが、より慣用的またはクリーンなソリューションを学びたいと思います。

  1. パターンは、nレベルのネストに対して繰り返し可能である必要があります。
  2. 囲んでいるルーチンをいつ解決するかを決定するには、promise.allまたは同様の手法を使用する必要があります。
  3. すべての要素が非同期呼び出しを行う必要があるとは限りません。したがって、ネストされたpromise.allでは、インデックスに基づいてJSON配列要素に単純に割り当てることはできません。それでも、ネストされたforEachでpromise.allのようなものを使用して、すべてのプロパティの割り当てが、含まれているルーチンを解決する前に行われていることを確認する必要があります。
  4. Bluebird promise libを使用していますが、これは必須ではありません

ここにいくつかの部分的なコードがあります-

var jsonItems = [];

items.forEach(function(item){

  var jsonItem = {};
  jsonItem.name = item.name;
  item.getThings().then(function(things){
  // or Promise.all(allItemGetThingCalls, function(things){

    things.forEach(function(thing, index){

      jsonItems[index].thingName = thing.name;
      if(thing.type === 'file'){

        thing.getFile().then(function(file){ //or promise.all?

          jsonItems[index].filesize = file.getSize();

これは私が改善したいワーキングソースへのリンクです。 github.com/pebanfield/change-view-service/blob/master/src/...
user3205931

1
私はあなたがブルーバードを使用しているサンプルを見ています、ブルーバードは実際にこの場合(同時)と(順次)であなたの人生をさらに簡単にします、またメモは非推奨です-私の答えのコードは約束を返すことでそれを避ける方法を示しています。約束はすべて戻り値に関するものです。Promise.mapPromise.eachPromise.defer
Benjamin Gruenbaum 2015

回答:


368

いくつかの単純なルールがあれば、非常に簡単です。

  • でプロミスを作成するときはいつでもそれをthen返します - 返さないプロミスは外部で待機しません。
  • あなたが複数の約束を作成するときはいつでも、.allそれらはすべての約束を待ち、それらのどれからのエラーも沈黙しません。
  • するたびに巣thenの、あなたは一般的に中央に戻ることができます - thenチェーンは通常、ほとんどの1つのレベルの深さです。
  • IOを実行するときはいつでも、プロミスを使用する必要があります。プロミス内にあるか、プロミスを使用して完了を通知する必要があります。

そしていくつかのヒント:

  • マッピングはを使用するより.mapも優れていfor/pushます。値を関数でマッピングする場合、mapアクションを1つずつ適用して結果を集約するという概念を簡潔に表現できます。
  • 並行性は、それが自由であれば順次実行よりも優れています -物事を同時にPromise.all実行し、物事を次々に実行するよりもそれらを待つ方が良いです-それぞれが次の前に待つ。

では、始めましょう。

var items = [1, 2, 3, 4, 5];
var fn = function asyncMultiplyBy2(v){ // sample async action
    return new Promise(resolve => setTimeout(() => resolve(v * 2), 100));
};
// map over forEach since it returns

var actions = items.map(fn); // run the function over all items

// we now have a promises array and we want to wait for it

var results = Promise.all(actions); // pass array of promises

results.then(data => // or just .then(console.log)
    console.log(data) // [2, 4, 6, 8, 10]
);

// we can nest this of course, as I said, `then` chains:

var res2 = Promise.all([1, 2, 3, 4, 5].map(fn)).then(
    data => Promise.all(data.map(fn))
).then(function(data){
    // the next `then` is executed after the promise has returned from the previous
    // `then` fulfilled, in this case it's an aggregate promise because of 
    // the `.all` 
    return Promise.all(data.map(fn));
}).then(function(data){
    // just for good measure
    return Promise.all(data.map(fn));
});

// now to get the results:

res2.then(function(data){
    console.log(data); // [16, 32, 48, 64, 80]
});

5
ああ、あなたの視点からのいくつかのルール :-)
Bergi

1
@Bergi誰かが実際にこれらのルールのリストと約束の短い背景を作成する必要があります。おそらくbluebirdjs.comでホストできます。
Benjamin Gruenbaum 2015

私はただ感謝するつもりはないので、この例は良さそうで、マップの提案が好きですが、一部だけが非同期メソッドを持つオブジェクトのコレクションに対して何をすべきですか?(上記の私のポイント3)私は、各要素の解析ロジックを関数に抽象化し、非同期呼び出し応答で解決するか、非同期呼び出しがなかった場合は単に解決することを考えていました。それは理にかなっていますか?
user3205931 2015

私はまた、map関数が、構築しているjsonオブジェクトと非同期呼び出しの結果の両方を返すようにする必要があります。これを行う方法も不明なので、ディレクトリを歩いているので、最後にすべてを再帰的にする必要があります。構造-私はまだこれを噛んでいますが、有料の仕事が邪魔になっています:(
user3205931

2
@ user3205931のプロミスは簡単ではなく、単純です。つまり、他のものほど馴染みが薄いのですが、いったんグロクを実行すると、はるかに使いやすくなります。それを手に入れましょう:)
Benjamin Gruenbaum

42

以下は、reduceを使用した簡単な例です。順次実行され、挿入順序を維持し、Bluebirdを必要としません。

/**
 * 
 * @param items An array of items.
 * @param fn A function that accepts an item from the array and returns a promise.
 * @returns {Promise}
 */
function forEachPromise(items, fn) {
    return items.reduce(function (promise, item) {
        return promise.then(function () {
            return fn(item);
        });
    }, Promise.resolve());
}

次のように使用します。

var items = ['a', 'b', 'c'];

function logItem(item) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            console.log(item);
            resolve();
        })
    });
}

forEachPromise(items, logItem).then(() => {
    console.log('done');
});

オプションのコンテキストをループに送ると便利です。コンテキストはオプションであり、すべての反復で共有されます。

function forEachPromise(items, fn, context) {
    return items.reduce(function (promise, item) {
        return promise.then(function () {
            return fn(item, context);
        });
    }, Promise.resolve());
}

あなたのpromise関数は次のようになります:

function logItem(item, context) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            console.log(item);
            context.itemCount++;
            resolve();
        })
    });
}

これをありがとう-他の人(さまざまなnpm libsを含む)がうまくいかなかったあなたの解決策は私のために働きました。これをnpmに公開しましたか?
SamF 2017年

ありがとうございました。関数はすべての約束が解決されていることを前提としています。拒否された約束をどのように処理しますか?また、価値のある成功した約束をどのように処理しますか?
oyalhi

@oyalhi「コンテキスト」を使用して、エラーにマップされた拒否された入力パラメーターの配列を追加することをお勧めします。残りの約束をすべて無視したい場合もあれば、そうでない場合もあるので、これは実際にユースケースごとです。戻り値についても、同様の方法を使用できます。
Steven Spungin 2018

1

私は同じ状況を経験しました。2つのPromise.All()を使用して解決しました。

私は本当に良い解決策だったと思うので、それをnpmに公開しました:https : //www.npmjs.com/package/promise-foreach

あなたのコードはこのようなものになると思います

var promiseForeach = require('promise-foreach')
var jsonItems = [];
promiseForeach.each(jsonItems,
    [function (jsonItems){
        return new Promise(function(resolve, reject){
            if(jsonItems.type === 'file'){
                jsonItems.getFile().then(function(file){ //or promise.all?
                    resolve(file.getSize())
                })
            }
        })
    }],
    function (result, current) {
        return {
            type: current.type,
            size: jsonItems.result[0]
        }
    },
    function (err, newList) {
        if (err) {
            console.error(err)
            return;
        }
        console.log('new jsonItems : ', newList)
    })

0

提示されたソリューションに追加するためだけに、私の場合、Firebaseから複数のデータをフェッチして製品のリストを取得したいと考えました。ここに私がそれをした方法があります:

useEffect(() => {
  const fn = p => firebase.firestore().doc(`products/${p.id}`).get();
  const actions = data.occasion.products.map(fn);
  const results = Promise.all(actions);
  results.then(data => {
    const newProducts = [];
    data.forEach(p => {
      newProducts.push({ id: p.id, ...p.data() });
    });
    setProducts(newProducts);
  });
}, [data]);
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.