Während Schleife mit Versprechungen

73

Was wäre der idiomatische Weg, um so etwas wie eine while-Schleife mit Versprechungen zu machen? Damit:

etwas tun, wenn die Bedingung noch besteht. Wiederholen Sie den Vorgang und wiederholen Sie dann etwas anderes.

dosomething.then(possilblydomoresomethings).then(finish)

Ich habe es so gemacht, dass ich mich gefragt habe, ob es bessere / idomatischere Wege gibt.

var q = require('q');

var index = 1;

var useless =  function(){
        var currentIndex = index;
        console.log(currentIndex)
        var deferred = q.defer();
        setTimeout(function(){
            if(currentIndex > 10)
                deferred.resolve(false);
            else deferred.resolve(true);
            },500);
        return deferred.promise;
    }

var control = function(cont){
        var deferred = q.defer();
        if(cont){
                index = index + 1;
                useless().then(control).then(function(){
                        deferred.resolve();
                    });
            }
         else deferred.resolve();
        return deferred.promise;
    }

var chain = useless().then(control).then(function(){console.log('done')});

Ausgabe: 1 2 3 4 5 6 7 8 9 10 11 fertig

Grummle
quelle
1
"Ich habe mich gefragt, ob es bessere / idomatischere Wege gibt?" Nein, Rekursion ist der richtige Weg.
Juandopazo
Wie würden Sie es ohne Rekursion machen? Ich hatte das Gefühl, dass eine Wiederholung wahrscheinlich nicht cool war, aber ich konnte nicht herausfinden, wie ich es ohne machen sollte. Irgendwelche Einsichten?
Grummle
Ich glaube nicht, dass Sie die Iteration ohne einen "Stop Here" -Mechanismus wie Taskjs verwenden können.
Juandopazo
Sie können eine Rekursion vermeiden, wenn in Ihrer Umgebung Async / Warten verfügbar ist. siehe meine aktualisierte Antwort unten.
Lawrence

Antworten:

19

Ich würde ein Objekt verwenden, um den Wert zu verpacken. Auf diese Weise können Sie eine doneEigenschaft haben, die die Schleife darüber informiert, dass Sie fertig sind.

// fn should return an object like
// {
//   done: false,
//   value: foo
// }
function loop(promise, fn) {
  return promise.then(fn).then(function (wrapper) {
    return !wrapper.done ? loop(Q(wrapper.value), fn) : wrapper.value;
  });
}

loop(Q.resolve(1), function (i) {
  console.log(i);
  return {
    done: i > 10,
    value: i++
  };
}).done(function () {
  console.log('done');
});
Juandopazo
quelle
9
Beachten Sie, dass dies nach einer ausreichend langen Laufzeit wahrscheinlich den gesamten verfügbaren Speicher belegt. In jeder Schleife des Versprechens scheint etwas enthalten zu sein, zumindest mit Q.
Asherah,
4
@juandopazo in Ihrem Beispiel Sie ändern sollten i++durch ++isonst werden Sie eine „Endlosschleife“ bekommen.
PauloASilva
Ich habe diese Lösung erfolgreich verwendet, sogar mit require ('promise');Bibliothek. Ich frage mich jetzt, ob eine nicht rekursive Lösung erstellt werden könnte, siehe stackoverflow.com/questions/36361827/…
Galder Zamarreño
Ashe, könnten Sie das Speicherproblem genauer beschreiben? Ist es mit Wrapper?
lgc_ustc
59

Hier ist eine wiederverwendbare Funktion, die meiner Meinung nach ziemlich klar ist.

var Q = require("q");

// `condition` is a function that returns a boolean
// `body` is a function that returns a promise
// returns a promise for the completion of the loop
function promiseWhile(condition, body) {
    var done = Q.defer();

    function loop() {
        // When the result of calling `condition` is no longer true, we are
        // done.
        if (!condition()) return done.resolve();
        // Use `when`, in case `body` does not return a promise.
        // When it completes loop again otherwise, if it fails, reject the
        // done promise
        Q.when(body(), loop, done.reject);
    }

    // Start running the loop in the next tick so that this function is
    // completely async. It would be unexpected if `body` was called
    // synchronously the first time.
    Q.nextTick(loop);

    // The promise
    return done.promise;
}


// Usage
var index = 1;
promiseWhile(function () { return index <= 11; }, function () {
    console.log(index);
    index++;
    return Q.delay(500); // arbitrary async
}).then(function () {
    console.log("done");
}).done();
Stuart K.
quelle
Das ist toll! Ich habe Ihr Beispiel für RSVP.js portiert: jsfiddle.net/wcW4r/1 Es könnte für Ember.js-Benutzer nützlich sein.
Miguelcobain
5
Neue Version mehr RSVP idiomatisch und mit Promise "Wrappern" in Körper und Zustand: jsfiddle.net/wcW4r/3
Miguelcobain
1
Dies kann Ausnahmen schlucken. Es ist besser, Q.fcall (body) .then (loop, done.reject) zu verwenden.
Tzanko Matev
Seltsam, das gibt mir einen Fehler: Fehler: ReferenceError: setTimeout ist beim Flush nicht definiert (vendor / q.js: 121: 21)
Rambatino
Dies hängt von der Rekursion ab. Wie wird sie dann skaliert? Was passiert beim Versuch, 1000 Anrufe zu tätigen?
Vitaly-t
33

Dies ist der einfachste Weg, den ich gefunden habe, um das Grundmuster auszudrücken: Sie definieren eine Funktion, die das Versprechen aufruft, das Ergebnis überprüft und sich dann entweder erneut selbst aufruft oder beendet.

const doSomething = value =>
  new Promise(resolve => 
    setTimeout(() => resolve(value >= 5 ? 'ok': 'no'), 1000))

const loop = value =>
  doSomething(value).then(result => {
    console.log(value)
    if (result === 'ok') {
      console.log('yay')      
    } else {
      return loop(value + 1)
    }
  })

loop(1).then(() => console.log('all done!'))

Sehen Sie es in Aktion auf JSBin

Wenn Sie ein Versprechen verwenden, das aufgelöst oder abgelehnt wird, würden Sie eine if-Klausel definieren thenund catchnicht verwenden.

Wenn Sie eine Reihe von Versprechungen hätten, würden Sie einfach loopjedes Mal wechseln , um die nächste zu verschieben oder zu knallen.


EDIT: Hier ist eine Version, die verwendet async/await, weil es 2018 ist:

const loop = async value => {
  let result = null
  while (result != 'ok') {
    console.log(value)
    result = await doSomething(value)
    value = value + 1
  }
  console.log('yay')
}

Sehen Sie es in Aktion auf CodePen

Wie Sie sehen können, wird eine normale while-Schleife und keine Rekursion verwendet.

Lawrence
quelle
2
Sehr schöne Lösung! Ich habe dies angepasst, um jedem Versprechen eine weitere Variable durch die Methodenkette zu übergeben. Vielen Dank für ein tolles einfaches Beispiel!
OzOli
1
hilfreicher als andere Antworten
Omar
13

Dies ist für Bluebird nicht q, aber da Sie q nicht speziell erwähnt haben, erwähnt der Autor im Bluebird-API-Dokument, dass die Rückgabe einer vielversprechenden Funktion idiomatischer wäre als die Verwendung von verzögerten Funktionen.

var Promise = require('bluebird');
var i = 0;

var counter = Promise.method(function(){
    return i++;
})

function getAll(max, results){
    var results = results || [];
    return counter().then(function(result){
        results.push(result);
        return (result < max) ? getAll(max, results) : results
    })
}

getAll(10).then(function(data){
    console.log(data);
})
Aarosil
quelle
1
Dies war hilfreich. Verwenden Sie eine rekursive Funktion, um die while-Schleife zu implementieren. Vielen Dank.
Steve Kehlet
1
Ich fand es auch nützlich, dies zu sehen, aber ich bin besorgt (für meine Verwendung - Iterieren aller Redis-Schlüssel mit SCAN), dass die Rekursion zu viel Stapel erzeugt und entweder für große Datenmengen fehlschlägt oder unnötigen Speicher verbraucht. Ich denke, es6-Generatoren könnten der Weg sein, den ich gehen muss.
Woche
Dies ist nicht genau das, wonach ich gesucht habe, aber es hat mir geholfen, eine Lösung zu finden.
Gustavo Straube
5

Da ich die Antwort von Stuart K nicht kommentieren kann, werde ich hier ein wenig hinzufügen. Basierend auf der Antwort von Stuart K können Sie es auf ein überraschend einfaches Konzept reduzieren : Verwenden Sie ein unerfülltes Versprechen wieder . Was er hat, ist im Wesentlichen:

  1. Erstellen Sie eine neue Instanz eines zurückgestellten Versprechens
  2. Definieren Sie Ihre Funktion, die Sie in einer Schleife aufrufen möchten
  3. Innerhalb dieser Funktion:
    1. Überprüfen Sie, ob Sie fertig sind. und wenn Sie das in Nr. 1 erstellte Versprechen lösen und es zurückgeben.
    2. Wenn Sie noch nicht fertig sind, weisen Sie Q an, das vorhandene Versprechen zu verwenden und die nicht erfüllte Funktion auszuführen, die die "rekursive" Funktion ist, oder schlagen Sie fehl, wenn sie gestorben ist. Q.when (Versprechen, yourFunction, failFunction)
  4. Nachdem Sie Ihre Funktion definiert haben, verwenden Sie Q, um die Funktion zum ersten Mal mit Q.nextTick (yourFunction) auszulösen.
  5. Geben Sie schließlich Ihr neues Versprechen an den Anrufer zurück (wodurch das Ganze gestartet wird).

Stuarts Antwort ist für eine allgemeinere Lösung, aber die Grundlagen sind fantastisch (sobald Sie erkennen, wie es funktioniert).

Millebi
quelle
4

Dieses Muster kann jetzt einfacher mit q-flow aufgerufen werden . Ein Beispiel für das obige Problem:

var q = require('q');
require('q-flow');
var index = 1;
q.until(function() {
  return q.delay(500).then(function() {
    console.log(index++);
    return index > 10;
  });
}).done(function() {
  return console.log('done');
});
Joe Hildebrand
quelle
Das ist großartig. Ist es möglich, dies mit Bluebird zu tun, sodass ich nicht zwei verschiedene Versprechen-Bibliotheken zusammen verwenden muss?
Trevor
3

Hier ist eine Erweiterung des PromisePrototyps, um das Verhalten einer forSchleife nachzuahmen . Es unterstützt Versprechen oder Sofortwerte für die Abschnitte Initialisierung, Bedingung, Schleifenkörper und Inkrement. Es bietet auch volle Unterstützung für Ausnahmen und keine Speicherlecks. Im Folgenden finden Sie ein Beispiel für die Verwendung.

var Promise = require('promise');


// Promise.loop([properties: object]): Promise()
//
//  Execute a loop based on promises. Object 'properties' is an optional
//  argument with the following fields:
//
//  initialization: function(): Promise() | any, optional
//
//      Function executed as part of the initialization of the loop. If
//      it returns a promise, the loop will not begin to execute until
//      it is resolved.
//
//      Any exception occurring in this function will finish the loop
//      with a rejected promise. Similarly, if this function returns a
//      promise, and this promise is reject, the loop finishes right
//      away with a rejected promise.
//
//  condition: function(): Promise(result: bool) | bool, optional
//
//      Condition evaluated in the beginning of each iteration of the
//      loop. The function should return a boolean value, or a promise
//      object that resolves with a boolean data value.
//
//      Any exception occurring during the evaluation of the condition
//      will finish the loop with a rejected promise. Similarly, it this
//      function returns a promise, and this promise is rejected, the
//      loop finishes right away with a rejected promise.
//
//      If no condition function is provided, an infinite loop is
//      executed.
//
//  body: function(): Promise() | any, optional
//
//      Function acting as the body of the loop. If it returns a
//      promise, the loop will not proceed until this promise is
//      resolved.
//
//      Any exception occurring in this function will finish the loop
//      with a rejected promise. Similarly, if this function returns a
//      promise, and this promise is reject, the loop finishes right
//      away with a rejected promise.
//
//  increment: function(): Promise() | any, optional
//
//      Function executed at the end of each iteration of the loop. If
//      it returns a promise, the condition of the loop will not be
//      evaluated again until this promise is resolved.
//
//      Any exception occurring in this function will finish the loop
//      with a rejected promise. Similarly, if this function returns a
//      promise, and this promise is reject, the loop finishes right
//      away with a rejected promise.
//
Promise.loop = function(properties)
{
    // Default values
    properties = properties || {};
    properties.initialization = properties.initialization || function() { };
    properties.condition = properties.condition || function() { return true; };
    properties.body = properties.body || function() { };
    properties.increment = properties.increment || function() { };

    // Start
    return new Promise(function(resolve, reject)
    {
        var runInitialization = function()
        {
            Promise.resolve().then(function()
            {
                return properties.initialization();
            })
            .then(function()
            {
                process.nextTick(runCondition);
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        var runCondition = function()
        {
            Promise.resolve().then(function()
            {
                return properties.condition();
            })
            .then(function(result)
            {
                if (result)
                    process.nextTick(runBody);
                else
                    resolve();
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        var runBody = function()
        {
            Promise.resolve().then(function()
            {
                return properties.body();
            })
            .then(function()
            {
                process.nextTick(runIncrement);
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        var runIncrement = function()
        {
            Promise.resolve().then(function()
            {
                return properties.increment();
            })
            .then(function()
            {
                process.nextTick(runCondition);
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        // Start running initialization
        process.nextTick(runInitialization);
    });
}


// Promise.delay(time: double): Promise()
//
//  Returns a promise that resolves after the given delay in seconds.
//
Promise.delay = function(time)
{
    return new Promise(function(resolve)
    {
        setTimeout(resolve, time * 1000);
    });
}


// Example
var i;
Promise.loop({
    initialization: function()
    {
        i = 2;
    },
    condition: function()
    {
        return i < 6;
    },
    body: function()
    {
        // Print "i"
        console.log(i);

        // Exception when 5 is reached
        if (i == 5)
            throw Error('Value of "i" reached 5');

        // Wait 1 second
        return Promise.delay(1);
    },
    increment: function()
    {
        i++;
    }
})
.then(function()
{
    console.log('LOOP FINISHED');
})
.catch(function(error)
{
    console.log('EXPECTED ERROR:', error.message);
});
user3707531
quelle
1
var Q = require('q')

var vetor  = ['a','b','c']

function imprimeValor(elements,initValue,defer){

    console.log( elements[initValue++] )
    defer.resolve(initValue)
    return defer.promise
}

function Qloop(initValue, elements,defer){

    Q.when( imprimeValor(elements, initValue, Q.defer()), function(initValue){

        if(initValue===elements.length){
            defer.resolve()
        }else{
            defer.resolve( Qloop(initValue,elements, Q.defer()) )
        }
    }, function(err){

        defer.reject(err)
    })

    return defer.promise
}

Qloop(0, vetor,Q.defer())
Lucas Rocha
quelle
1

Ich benutze jetzt Folgendes:

function each(arr, work) {
  function loop(arr, i) {
    return new Promise(function(resolve, reject) {
      if (i >= arr.length) {resolve();}
      else try {
        Promise.resolve(work(arr[i], i)).then(function() { 
          resolve(loop(arr, i+1))
        }).catch(reject);
      } catch(e) {reject(e);}
    });
  }
  return loop(arr, 0);
}

Dies akzeptiert ein Array arrund eine Funktion workund gibt a zurück Promise. Die angegebene Funktion wird einmal für jedes Element im Array aufgerufen und übergibt das aktuelle Element und seinen Index im Array. Es kann synchron oder asynchron sein. In diesem Fall muss ein Versprechen zurückgegeben werden.

Sie können es so verwenden:

var items = ['Hello', 'cool', 'world'];
each(items, function(item, idx) {
    // this could simply be sync, but can also be async
    // in which case it must return a Promise
    return new Promise(function(resolve){
        // use setTimeout to make this async
        setTimeout(function(){
            console.info(item, idx);
            resolve();
        }, 1000);
    });
})
.then(function(){
    console.info('DONE');
})
.catch(function(error){
    console.error('Failed', error);
})

Jedes Element im Array wird nacheinander behandelt. Sobald alle behandelt sind, wird der Code für .then()ausgeführt, oder, falls ein Fehler aufgetreten ist, der Code für .catch(). Innerhalb der workFunktion können Sie throwein Error(im Fall von synchronen Funktionen) oder rejectden Promise(im Fall von Asynchron - Funktionen) die Schleife abzubrechen.

Stijn de Witt
quelle
0

Mit dem ES6-Versprechen habe ich mir das ausgedacht. Es verkettet die Versprechen und gibt ein Versprechen zurück. Es ist technisch gesehen keine while-Schleife, zeigt aber, wie Versprechen synchron durchlaufen werden.

function chain_promises(list, fun) {
    return list.reduce(
        function (promise, element) {
            return promise.then(function () {
                // I only needed to kick off some side-effects. If you need to get
                // a list back, you would append to it here. Or maybe use
                // Array.map instead of Array.reduce.
                fun(element);
            });
    	},
        // An initial promise just starts things off.
        Promise.resolve(true)
    );
}

// To test it...

function test_function (element) {
    return new Promise(function (pass, _fail) {
        console.log('Processing ' + element);
        pass(true);
    });
}

chain_promises([1, 2, 3, 4, 5], test_function).then(function () {
    console.log('Done.');
});

Hier ist meine Geige.

mqsoh
quelle
1
Tipp: Verwenden Sie Promise.resolve(true)anstelle des new PromiseKonstruktors
Bergi
0

Ich dachte, ich könnte genauso gut meinen Hut mit ES6 Promises in den Ring werfen ...

function until_success(executor){
    var before_retry = undefined;
    var outer_executor = function(succeed, reject){
        var rejection_handler = function(err){
            if(before_retry){
                try {
                    var pre_retry_result = before_retry(err);
                    if(pre_retry_result)
                        return succeed(pre_retry_result);
                } catch (pre_retry_error){
                    return reject(pre_retry_error);
                }
            }
            return new Promise(executor).then(succeed, rejection_handler);                
        }
        return new Promise(executor).then(succeed, rejection_handler);
    }

    var outer_promise = new Promise(outer_executor);
    outer_promise.before_retry = function(func){
        before_retry = func;
        return outer_promise;
    }
    return outer_promise;
}

Das executorArgument ist das gleiche wie das an einen PromiseKonstruktor übergebene, wird jedoch wiederholt aufgerufen, bis der erfolgreiche Rückruf ausgelöst wird. Die before_retryFunktion ermöglicht die benutzerdefinierte Fehlerbehandlung bei fehlgeschlagenen Versuchen. Wenn es einen wahrheitsgemäßen Wert zurückgibt, wird es als eine Form des Erfolgs angesehen und die "Schleife" endet, mit dieser Wahrheit als Ergebnis. Wenn keine before_retryFunktion registriert ist oder einen Falsey-Wert zurückgibt, wird die Schleife für eine weitere Iteration ausgeführt. Die dritte Option ist, dass die before_retryFunktion selbst einen Fehler auslöst. In diesem Fall endet die "Schleife" und übergibt diesen Fehler als Fehler.


Hier ist ein Beispiel:

var counter = 0;
function task(succ, reject){
    setTimeout(function(){
        if(++counter < 5)
            reject(counter + " is too small!!");
        else
            succ(counter + " is just right");
    }, 500); // simulated async task
}

until_success(task)
        .before_retry(function(err){
            console.log("failed attempt: " + err);
            // Option 0: return falsey value and move on to next attempt
            // return

            // Option 1: uncomment to get early success..
            //if(err === "3 is too small!!") 
            //    return "3 is sort of ok"; 

            // Option 2: uncomment to get complete failure..
            //if(err === "3 is too small!!") 
            //    throw "3rd time, very unlucky"; 
  }).then(function(val){
       console.log("finally, success: " + val);
  }).catch(function(err){
       console.log("it didn't end well: " + err);
  })

Ausgabe für Option 0:

failed attempt: 1 is too small!!
failed attempt: 2 is too small!!
failed attempt: 3 is too small!!
failed attempt: 4 is too small!!
finally, success: 5 is just right

Ausgabe für Option 1:

failed attempt: 1 is too small!!
failed attempt: 2 is too small!!
failed attempt: 3 is too small!!
finally, success: 3 is sort of ok

Ausgabe für Option 2:

failed attempt: 1 is too small!!
failed attempt: 2 is too small!!
failed attempt: 3 is too small!!
it didn't end well: 3rd time, very unlucky
Dan-Mann
quelle
0

Viele Antworten hier und was Sie erreichen wollen, ist nicht sehr praktisch. aber das sollte funktionieren. Dies wurde in einer aws-Lambda-Funktion implementiert, mit Node.js 10 dauert es bis zum Timeout der Funktion. Es kann auch eine anständige Menge an Speicher verbrauchen.

exports.handler = async (event) => {
  let res = null;
  while (true) {
    try{
     res = await dopromise();
    }catch(err){
     res = err;
    }
    console.log(res);
   }//infinite will time out
  };

  function dopromise(){
   return new Promise((resolve, reject) => {
    //do some logic
    //if error reject
        //reject('failed');
    resolve('success');
  });
}

Auf Lambda getestet und über 5 Minuten gut laufen. Aber wie von anderen gesagt, ist dies keine gute Sache.

Hans-Eric Lippke
quelle
-1

Ich habe ein Modul geschrieben, mit dem Sie verkettete Schleifen asynchroner Aufgaben mit Versprechungen ausführen können. Es basiert auf der obigen Antwort von juandopazo

/**
 * Should loop over a task function which returns a "wrapper" object
 * until wrapper.done is true. A seed value wrapper.seed is propagated to the
 * next run of the loop.
 *
 * todo/maybe? Reject if wrapper is not an object with done and seed keys.
 *
 * @param {Promise|*} seed
 * @param {Function} taskFn
 *
 * @returns {Promise.<*>}
 */
function seedLoop(seed, taskFn) {
  const seedPromise = Promise.resolve(seed);

  return seedPromise
    .then(taskFn)
    .then((wrapper) => {
      if (wrapper.done) {
        return wrapper.seed;
      }

      return seedLoop(wrapper.seed, taskFn);
    });
}

// A super simple example of counting to ten, which doesn't even
// do anything asynchronous, but if it did, it should resolve to 
// a promise that returns the { done, seed } wrapper object for the
// next call of the countToTen task function.
function countToTen(count) {
  const done = count > 10;
  const seed = done ? count : count + 1;

  return {done, seed};
}

seedLoop(1, countToTen).then((result) => {
  console.log(result); // 11, the first value which was over 10.
});

https://github.com/CascadeEnergy/promise-seedloop

nackjicholson
quelle