Richtiger Weg, um Loops für Versprechen zu schreiben.

116

Wie kann eine Schleife korrekt erstellt werden, um sicherzustellen, dass der folgende Versprechensaufruf und die verkettete logger.log (res) synchron durch die Iteration ausgeführt werden? (Drossel)

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

Ich habe den folgenden Weg versucht (Methode von 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');
}); 

Es scheint zwar zu funktionieren, aber ich glaube nicht, dass es die Reihenfolge des Aufrufs von logger.log (res) garantiert ;

Irgendwelche Vorschläge?

user2127480
quelle
1
Der Code sieht für mich gut aus (Rekursion mit der loopFunktion ist der Weg, um synchrone Schleifen zu machen). Warum gibt es Ihrer Meinung nach keine Garantie?
Hugomg
db.getUser (email) wird garantiert in der richtigen Reihenfolge aufgerufen. Da db.getUser () selbst ein Versprechen ist, bedeutet ein sequentieller Aufruf nicht unbedingt, dass die Datenbankabfragen für 'E-Mail' aufgrund der asynchronen Funktion des Versprechens sequentiell ausgeführt werden. Daher wird die Datei logger.log (res) aufgerufen, je nachdem, welche Abfrage zuerst beendet wird.
user2127480
1
@ user2127480: Aber die nächste Iteration der Schleife wird erst nach Ablauf des Versprechens nacheinander aufgerufen. So funktioniert dieser whileCode?
Bergi

Antworten:

78

Ich glaube nicht, dass dies die Reihenfolge des Aufrufs von logger.log (res) garantiert.

Eigentlich schon. Diese Anweisung wird vor dem resolveAufruf ausgeführt.

Irgendwelche Vorschläge?

Viele. Das Wichtigste ist, dass Sie das Antipattern "Versprechen versprechen" manuell verwenden - tun Sie es einfach

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

Zweitens whilekönnte diese Funktion stark vereinfacht werden:

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

Drittens würde ich keine whileSchleife (mit einer Abschlussvariablen) verwenden, sondern eine forSchleife:

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'));
Bergi
quelle
2
Hoppla. Außer das actionnimmt valueals Argument in promiseFor. SO würde ich nicht so eine kleine Bearbeitung machen lassen. Danke, es ist sehr hilfreich und elegant.
Gordon
1
@ Roamer-1888: Vielleicht ist die Terminologie etwas seltsam, aber ich meine, dass eine whileSchleife einen globalen Zustand testet, während fordie Iterationsvariable (Zähler) einer Schleife an den Schleifenkörper selbst gebunden ist. Tatsächlich habe ich einen funktionaleren Ansatz verwendet, der eher einer Fixpunktiteration als einer Schleife ähnelt. Überprüfen Sie den Code erneut, der valueParameter ist anders.
Bergi
2
OK, ich sehe es jetzt. Da das .bind()Neue verschleiert wird value, denke ich, dass ich mich dafür entscheiden könnte, die Funktion aus Gründen der Lesbarkeit zu verlängern. Und sorry , wenn ich dick ist, aber wenn promiseForund promiseWhilekoexistieren nicht, dann , wie nennt man das andere?
Roamer-1888
2
@herve Sie können weglassen es grundsätzlich und ersetzen die return …durch return Promise.resolve(…). Wenn Sie zusätzliche Schutzmaßnahmen gegen eine Ausnahme benötigen conditionoder dieseactionPromise.methodreturn Promise.resolve().then(() => { … })
auslösen
2
@herve Eigentlich sollte das sein Promise.resolve().then(action).…oder Promise.resolve(action()).…, Sie müssen den Rückgabewert vonthen
Bergi
134

Wenn Sie wirklich eine allgemeine promiseWhen()Funktion für diesen und andere Zwecke wünschen , dann tun Sie dies auf jeden Fall mit Bergis Vereinfachungen. Aufgrund der Art und Weise, wie Versprechen funktionieren, ist das Übergeben von Rückrufen auf diese Weise im Allgemeinen nicht erforderlich und zwingt Sie, durch komplexe kleine Reifen zu springen.

Soweit ich das beurteilen kann, versuchen Sie:

  • asynchrones Abrufen einer Reihe von Benutzerdetails für eine Sammlung von E-Mail-Adressen (zumindest ist dies das einzige sinnvolle Szenario).
  • Dazu wird eine .then()Kette durch Rekursion aufgebaut.
  • um die ursprüngliche Bestellung bei der Bearbeitung der zurückgegebenen Ergebnisse beizubehalten.

Das so definierte Problem ist tatsächlich das unter "The Collection Kerfuffle" in Promise Anti-Patterns diskutierte , das zwei einfache Lösungen bietet:

  • parallele asynchrone Aufrufe mit Array.prototype.map()
  • serielle asynchrone Aufrufe mit Array.prototype.reduce().

Der parallele Ansatz führt (direkt) zu dem Problem, das Sie vermeiden möchten - dass die Reihenfolge der Antworten ungewiss ist. Der serielle Ansatz erstellt die erforderliche .then()Kette - flach - ohne Rekursion.

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

Rufen Sie wie folgt an:

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

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

Wie Sie sehen können, ist die hässliche äußere Variable countoder die damit verbundene conditionFunktion nicht erforderlich . Die Grenze (von 10 in der Frage) wird vollständig durch die Länge des Arrays bestimmt arrayOfEmailAddys.

Roamer-1888
quelle
16
fühlt sich so an, sollte dies die ausgewählte Antwort sein. anmutiger und sehr wiederverwendbarer Ansatz.
Ken
1
Weiß jemand, ob sich ein Fang an die Eltern weitergeben würde? Wenn beispielsweise db.getUser fehlschlagen würde, würde sich der (Zurückweisungs-) Fehler wieder ausbreiten?
Wayofthefuture
@wayofthefuture, nein. Stellen Sie sich das so vor ... Sie können die Geschichte nicht ändern.
Roamer-1888
4
Danke für die Antwort. Dies sollte die akzeptierte Antwort sein.
klvs
1
@ Roamer-1888 Mein Fehler, ich habe die ursprüngliche Frage falsch verstanden. Ich habe (persönlich) nach einer Lösung gesucht, bei der die anfängliche Liste, die Sie reduzieren müssen, mit der Abwicklung Ihrer Anforderungen wächst (es ist eine AbfrageMehr einer Datenbank). In diesem Fall fand ich die Idee, mit einem Generator eine recht schöne Trennung von (1) der bedingten Verlängerung der Versprechenskette und (2) dem Verbrauch der zurückgegebenen Ergebnisse zu verwenden.
JHP
40

So mache ich es mit dem Standard-Promise-Objekt.

// 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
quelle
Tolle Antwort @youngwerth
Jam Risser
3
Wie sende ich Parameter auf diese Weise?
Akash Khan
4
@ Khan auf der Kette = chain.then (func) Linie, könnten Sie entweder tun: chain = chain.then(func.bind(null, "...your params here")); oder chain = chain.then(() => func("your params here"));
youngwerth
9

Gegeben

  • asyncFn-Funktion
  • Reihe von Elementen

Erforderlich

  • verspreche Verkettung .then () ist in Reihe (in Reihenfolge)
  • native es6

Lösung

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())
Kamran
quelle
2
Wenn asynces in JavaScript zu einem reservierten Wort wird, kann dies die Klarheit beim Umbenennen dieser Funktion erhöhen.
Hippietrail
Ist es nicht auch so, dass ein Fettpfeil funktioniert, ohne dass ein Körper in geschweiften Klammern einfach das zurückgibt, was der Ausdruck dort auswertet? Das würde den Code prägnanter machen. Ich könnte auch einen Kommentar hinzufügen, der besagt, dass er currentnicht verwendet wird.
Hippietrail
2
das ist der richtige Weg!
Teleme.io
4

Es gibt einen neuen Weg, dies zu lösen, und zwar mithilfe von 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

tomasgvivo
quelle
Vielen Dank, dies beinhaltet nicht die Verwendung eines Frameworks (Bluebird).
Rolf
3

Bergis vorgeschlagene Funktion ist wirklich nett:

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

Trotzdem möchte ich eine winzige Ergänzung machen, die Sinn macht, wenn ich Versprechen verwende:

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

Auf diese Weise kann die while-Schleife in eine Versprechenskette eingebettet werden und mit lastValue aufgelöst werden (auch wenn die action () nie ausgeführt wird). Siehe Beispiel:

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

Ich würde so etwas machen:

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]); 
  }  
});

Auf diese Weise ist dataAll ein geordnetes Array aller zu protokollierenden Elemente. Der Protokollvorgang wird ausgeführt, wenn alle Versprechen erfüllt sind.

Claudio
quelle
Promise.all ruft gleichzeitig die Willensversprechen auf. Die Reihenfolge der Fertigstellung kann sich also ändern. Die Frage fragt nach verketteten Versprechungen. Die Reihenfolge der Fertigstellung sollte daher nicht geändert werden.
Canbax
Bearbeiten 1: Sie müssen Promise.all überhaupt nicht aufrufen. Solange die Versprechen abgefeuert werden, werden sie parallel ausgeführt.
Canbax
1

Verwenden Sie asynchron und warten Sie (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'));
}

}
ramachandrareddy reddam
quelle
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);
});
Tengiz
quelle
0

Wie wäre es mit BlueBird ?

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

Hier ist eine andere Methode (ES6 mit Standardversprechen). Verwendet Exit-Kriterien vom Typ lodash / underscore (return === false). Beachten Sie, dass Sie in Optionen, die in doOne () ausgeführt werden sollen, problemlos eine exitIf () -Methode hinzufügen können.

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)
    })
};
GrumpyGary
quelle
0

Wenn Sie das Standardversprechenobjekt verwenden und das Versprechen haben, werden die Ergebnisse zurückgegeben.

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)
})
Chris Blaser
quelle
0

Nehmen Sie zuerst ein Versprechen-Array (Versprechen-Array) und lösen Sie dieses Versprechen-Array anschließend mit auf 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
}
ramachandrareddy reddam
quelle