promiseのループを記述する正しい方法。


116

ループを正しく構築して、次のpromise呼び出しとチェーンされたlogger.log(res)が反復を通じて同期的に実行されるようにする方法は?(青い鳥)

db.getUser(email).then(function(res) { logger.log(res); }); // this is a promise

私は次の方法を試しました(http://blog.victorquinn.com/javascript-promise-while-loopからの方法)

var Promise = require('bluebird');

var promiseWhile = function(condition, action) {
    var resolver = Promise.defer();

    var loop = function() {
        if (!condition()) return resolver.resolve();
        return Promise.cast(action())
            .then(loop)
            .catch(resolver.reject);
    };

    process.nextTick(loop);

    return resolver.promise;
});

var count = 0;
promiseWhile(function() {
    return count < 10;
}, function() {
    return new Promise(function(resolve, reject) {
        db.getUser(email)
          .then(function(res) { 
              logger.log(res); 
              count++;
              resolve();
          });
    }); 
}).then(function() {
    console.log('all done');
}); 

動作しているように見えますが、logger.log(res)の呼び出し順序が保証されているとは思いません

助言がありますか?


1
コードは私には問題なく見えます(loop関数での再帰は同期ループを行う方法です)。なぜ保証がないと思いますか?
hugomg 14

db.getUser(email)は順番に呼び出されることが保証されています。ただし、db.getUser()自体はpromiseであるため、それを順次呼び出しても、promiseの非同期機能により、「電子メール」のデータベースクエリが順次実行されるとは限りません。したがって、最初に終了するクエリに応じて、logger.log(res)が呼び出されます。
user2127480 14

1
@ user2127480:しかし、ループの次の反復は、promiseが解決された後にのみ順次呼び出されwhileます。つまり、そのコードはどのように機能しますか?
Bergi 14

回答:


78

logger.log(res)の呼び出し順序が保証されているとは思いません。

実際にはそうです。そのステートメントは、resolve呼び出しの前に実行されます。

助言がありますか?

たくさん。最も重要なのは、あなたの使用であるアンチパターン-手動で作成-約束 -ちょうどのみ行います

promiseWhile(…, function() {
    return db.getUser(email)
             .then(function(res) { 
                 logger.log(res); 
                 count++;
             });
})…

次に、そのwhile機能は大幅に簡略化できます。

var promiseWhile = Promise.method(function(condition, action) {
    if (!condition()) return;
    return action().then(promiseWhile.bind(null, condition, action));
});

3番目に、whileループ(クロージャー変数を含む)ではなく、forループを使用します。

var promiseFor = Promise.method(function(condition, action, value) {
    if (!condition(value)) return value;
    return action(value).then(promiseFor.bind(null, condition, action));
});

promiseFor(function(count) {
    return count < 10;
}, function(count) {
    return db.getUser(email)
             .then(function(res) { 
                 logger.log(res); 
                 return ++count;
             });
}, 0).then(console.log.bind(console, 'all done'));

2
おっとっと。ただし、それactionはのvalue引数として使用されpromiseForます。SOでは、このような小さな編集はできません。おかげで、とても親切でエレガントです。
ゴードン

1
@ Roamer-1888:用語が少し変わっているかもしれませんが、whileループがforループ本体自体にバインドされた反復変数(カウンター)を持っている間に、ループがグローバル状態をテストすることを意味します。実際、私はループではなく、フィックスポイント反復のように見える、より機能的なアプローチを使用しました。もう一度コードを確認してくださいvalue。パラメータが異なります。
ベルギ14

2
はい、わかりました。.bind()新しいを難読化するので、value読みやすくするために関数を手書きで書くことを選択するかもしれません。私は厚めのものが、場合てる場合と申し訳ありませんpromiseForpromiseWhile共存しない場合、どのように一方が他方を呼び出すのですか?
Roamer-1888 14年

2
あなたは基本的にはそれを省略し、置き換えることができ@herve return …return Promise.resolve(…)。(それを提供するなどの)例外に対する追加の保護手段conditionまたはaction例外のスローが必要な場合は、関数本体全体をPromise.methodreturn Promise.resolve().then(() => { … })
Bergi

2
@herve実際には、Promise.resolve().then(action).…またはPromise.resolve(action()).…である必要がありますthen
Bergi

134

promiseWhen()この目的やその他の目的で一般的な関数が本当に必要な場合は、Bergiの単純化を使用して、必ずそうしてください。ただし、promiseが機能する方法のため、この方法でコールバックを渡すことは一般に不要であり、複雑な小さなフープをジャンプする必要があります。

私があなたがしようとしていると言える限り:

  • 電子メールアドレスのコレクションの一連のユーザー詳細を非同期でフェッチするには(少なくとも、これが意味のある唯一のシナリオです)。
  • これを行うには、.then()再帰によってチェーンを構築します。
  • 返された結果を処理するときに元の順序を維持する。

このように定義すると、問題は実際にはPromise Anti-patternsの「The Collection Kerfuffle」で説明されている問題であり、2つの単純なソリューションが提供されます。

  • を使用した並列非同期呼び出し Array.prototype.map()
  • を使用したシリアル非同期呼び出しArray.prototype.reduce()

並列アプローチでは、(単純に)回避しようとしている問題(応答の順序が不明確)が発生します。逐次アプローチは、必要な.then()チェーンを構築します-フラット-再帰なし。

function fetchUserDetails(arr) {
    return arr.reduce(function(promise, email) {
        return promise.then(function() {
            return db.getUser(email).done(function(res) {
                logger.log(res);
            });
        });
    }, Promise.resolve());
}

次のように呼び出します。

//Compose here, by whatever means, an array of email addresses.
var arrayOfEmailAddys = [...];

fetchUserDetails(arrayOfEmailAddys).then(function() {
    console.log('all done');
});

ご覧のとおり、醜い外部変数countやそれに関連するcondition関数は必要ありません。制限(質問では10)は、配列の長さによって完全に決まりますarrayOfEmailAddys


16
これが選択された答えであるように感じます。優雅で再利用可能なアプローチ。

1
キャッチが親に伝播するかどうか誰かが知っていますか?たとえば、db.getUserが失敗した場合、(拒否)エラーは元に戻りますか?
wayofthefuture 2016

@wayofthefuture、いいえ。このように考えてください.....履歴を変更することはできません。
Roamer-1888 2016年

4
答えてくれてありがとう。これは受け入れられる答えになるはずです。
klvs 2017年

1
@ Roamer-1888私の間違い、私は元の質問を読み間違えました。私は(個人的に)要求が解決するにつれ、reduceに必要な最初のリスト(DBのqueryMore)が大きくなるソリューションを探していました。この場合、ジェネレーターでreduceを使用するというアイデアは、(1)promiseチェーンの条件付き拡張と(2)返された結果の消費のかなり良い分離であることがわかりました。
jhp

40

標準のPromiseオブジェクトを使用してこれを行う方法を次に示します。

// Given async function sayHi
function sayHi() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('Hi');
      resolve();
    }, 3000);
  });
}

// And an array of async functions to loop through
const asyncArray = [sayHi, sayHi, sayHi];

// We create the start of a promise chain
let chain = Promise.resolve();

// And append each function in the array to the promise chain
for (const func of asyncArray) {
  chain = chain.then(func);
}

// Output:
// Hi
// Hi (After 3 seconds)
// Hi (After 3 more seconds)

すばらしい回答@youngwerth
Jam Risser

3
この方法でパラメータを送信する方法は?
Akash khan 2016

4
@khanはchain = chain.then(func)行で、次のいずれかを実行できます。chain = chain.then(func.bind(null, "...your params here")); または chain = chain.then(() => func("your params here"));
youngwerth

9

与えられた

  • asyncFn関数
  • アイテムの配列

必須

  • 順番に.then()の連鎖を約束する
  • ネイティブes6

解決

let asyncFn = (item) => {
  return new Promise((resolve, reject) => {
    setTimeout( () => {console.log(item); resolve(true)}, 1000 )
  })
}

// asyncFn('a')
// .then(()=>{return async('b')})
// .then(()=>{return async('c')})
// .then(()=>{return async('d')})

let a = ['a','b','c','d']

a.reduce((previous, current, index, array) => {
  return previous                                    // initiates the promise chain
  .then(()=>{return asyncFn(array[index])})      //adds .then() promise for each item
}, Promise.resolve())

2
asyncJavaScriptで予約語になりそうな場合は、ここでその関数の名前を変更する必要がある場合があります。
ヒッピートレイル2016

また、中括弧内の本体なしでファットアロー関数が単純にそこにある式が評価するものを返すわけではありませんか?これにより、コードがより簡潔になります。current未使用であることを示すコメントも追加する場合があります。
ヒッピートレイル2016

2
これが正しい方法です!
teleme.io 2017年

4

これを解決する新しい方法があり、それはasync / awaitを使用することです。

async function myFunction() {
  while(/* my condition */) {
    const res = await db.getUser(email);
    logger.log(res);
  }
}

myFunction().then(() => {
  /* do other stuff */
})

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function https://ponyfoo.com/articles/understanding-javascript-async-await


ありがとうございます。これにはフレームワーク(bluebird)の使用は含まれません。
Rolf

3

Bergiの提案する関数は本当に素晴らしいです:

var promiseWhile = Promise.method(function(condition, action) {
      if (!condition()) return;
    return action().then(promiseWhile.bind(null, condition, action));
});

それでも、Promiseを使用するときに意味のある小さな追加を行いたいです。

var promiseWhile = Promise.method(function(condition, action, lastValue) {
  if (!condition()) return lastValue;
  return action().then(promiseWhile.bind(null, condition, action));
});

このようにして、whileループをpromiseチェーンに埋め込み、lastValueで解決できます(action()が実行されない場合も)。例を参照してください:

var count = 10;
util.promiseWhile(
  function condition() {
    return count > 0;
  },
  function action() {
    return new Promise(function(resolve, reject) {
      count = count - 1;
      resolve(count)
    })
  },
  count)

3

私はこのようなものを作ります:

var request = []
while(count<10){
   request.push(db.getUser(email).then(function(res) { return res; }));
   count++
};

Promise.all(request).then((dataAll)=>{
  for (var i = 0; i < dataAll.length; i++) {

      logger.log(dataAll[i]); 
  }  
});

このように、dataAllはログに記録するすべての要素の順序付き配列です。そして、ログ操作はすべての約束が行われたときに実行されます。


Promise.allは、willとpromiseを同時に呼び出します。そのため、完了の順序が変わる可能性があります。質問は連鎖した約束を求めています。したがって、完了の順序は変更しないでください。
canbax

編集1:Promise.allを呼び出す必要はまったくありません。promiseが実行される限り、並行して実行されます。
canbax 2018

1

asyncとawait(es6)を使用します。

function taskAsync(paramets){
 return new Promise((reslove,reject)=>{
 //your logic after reslove(respoce) or reject(error)
})
}

async function fName(){
let arry=['list of items'];
  for(var i=0;i<arry.length;i++){
   let result=await(taskAsync('parameters'));
}

}

0
function promiseLoop(promiseFunc, paramsGetter, conditionChecker, eachFunc, delay) {
    function callNext() {
        return promiseFunc.apply(null, paramsGetter())
            .then(eachFunc)
    }

    function loop(promise, fn) {
        if (delay) {
            return new Promise(function(resolve) {
                setTimeout(function() {
                    resolve();
                }, delay);
            })
                .then(function() {
                    return promise
                        .then(fn)
                        .then(function(condition) {
                            if (!condition) {
                                return true;
                            }
                            return loop(callNext(), fn)
                        })
                });
        }
        return promise
            .then(fn)
            .then(function(condition) {
                if (!condition) {
                    return true;
                }
                return loop(callNext(), fn)
            })
    }

    return loop(callNext(), conditionChecker);
}


function makeRequest(param) {
    return new Promise(function(resolve, reject) {
        var req = https.request(function(res) {
            var data = '';
            res.on('data', function (chunk) {
                data += chunk;
            });
            res.on('end', function () {
                resolve(data);
            });
        });
        req.on('error', function(e) {
            reject(e);
        });
        req.write(param);
        req.end();
    })
}

function getSomething() {
    var param = 0;

    var limit = 10;

    var results = [];

    function paramGetter() {
        return [param];
    }
    function conditionChecker() {
        return param <= limit;
    }
    function callback(result) {
        results.push(result);
        param++;
    }

    return promiseLoop(makeRequest, paramGetter, conditionChecker, callback)
        .then(function() {
            return results;
        });
}

getSomething().then(function(res) {
    console.log('results', res);
}).catch(function(err) {
    console.log('some error along the way', err);
});

0

BlueBirdを使用したこれはどうですか?

function fetchUserDetails(arr) {
    return Promise.each(arr, function(email) {
        return db.getUser(email).done(function(res) {
            logger.log(res);
        });
    });
}

0

これが別の方法です(ES6 w / std Promise)。lodash / underscoreタイプの終了基準を使用します(return === false)。doOne()で実行するオプションにexitIf()メソッドを簡単に追加できることに注意してください。

const whilePromise = (fnReturningPromise,options = {}) => { 
    // loop until fnReturningPromise() === false
    // options.delay - setTimeout ms (set to 0 for 1 tick to make non-blocking)
    return new Promise((resolve,reject) => {
        const doOne = () => {
            fnReturningPromise()
            .then((...args) => {
                if (args.length && args[0] === false) {
                    resolve(...args);
                } else {
                    iterate();
                }
            })
        };
        const iterate = () => {
            if (options.delay !== undefined) {
                setTimeout(doOne,options.delay);
            } else {
                doOne();
            }
        }
        Promise.resolve()
        .then(iterate)
        .catch(reject)
    })
};

0

標準のpromiseオブジェクトを使用し、promiseで結果を返します。

function promiseMap (data, f) {
  const reducer = (promise, x) =>
    promise.then(acc => f(x).then(y => acc.push(y) && acc))
  return data.reduce(reducer, Promise.resolve([]))
}

var emails = []

function getUser(email) {
  return db.getUser(email)
}

promiseMap(emails, getUser).then(emails => {
  console.log(emails)
})

0

最初の約束のテイク配列(約束の配列)と決意した後、これらの約束の配列が使用Promise.all(promisearray)

var arry=['raju','ram','abdul','kruthika'];

var promiseArry=[];
for(var i=0;i<arry.length;i++) {
  promiseArry.push(dbFechFun(arry[i]));
}

Promise.all(promiseArry)
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
     console.log(error);
  });

function dbFetchFun(name) {
  // we need to return a  promise
  return db.find({name:name}); // any db operation we can write hear
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.