Versprechen nacheinander auflösen (dh nacheinander)?


269

Betrachten Sie den folgenden Code, der ein Array von Dateien seriell / sequentiell liest. readFilesgibt ein Versprechen zurück, das erst aufgelöst wird, wenn alle Dateien nacheinander gelesen wurden.

var readFile = function(file) {
  ... // Returns a promise.
};

var readFiles = function(files) {
  return new Promise((resolve, reject) => 

    var readSequential = function(index) {
      if (index >= files.length) {
        resolve();
      } else {
        readFile(files[index]).then(function() {
          readSequential(index + 1);
        }).catch(reject);
      }
    };

   readSequential(0); // Start!

  });
};

Der obige Code funktioniert, aber ich mag es nicht, eine Rekursion durchführen zu müssen, damit die Dinge nacheinander ablaufen. Gibt es eine einfachere Möglichkeit, diesen Code neu zu schreiben, damit ich meine seltsame readSequentialFunktion nicht verwenden muss?

Ursprünglich habe ich versucht , zu verwenden Promise.all, sondern dass alle der verursacht readFileAnrufe gleichzeitig passieren, das ist nicht , was ich will:

var readFiles = function(files) {
  return Promise.all(files.map(function(file) {
    return readFile(file);
  }));
};

2
Alles, was auf den Abschluss eines vorherigen asynchronen Vorgangs warten muss, muss in einem Rückruf ausgeführt werden. Das Verwenden von Versprechungen ändert daran nichts. Sie brauchen also die Rekursion.
Barmar

1
Zu Ihrer Information, dies ist technisch gesehen keine Rekursion, da kein Stapelrahmen aufgebaut wird. Der vorherige readFileSequential()ist bereits zurückgekehrt, bevor der nächste aufgerufen wird (da er asynchron ist, wird er abgeschlossen, lange nachdem der ursprüngliche Funktionsaufruf bereits zurückgegeben wurde).
jfriend00

1
@ jfriend00 Für die Rekursion ist keine Stapelrahmenakkumulation erforderlich - nur Selbstreferenz. Dies ist jedoch nur eine technische Angelegenheit.
Benjamin Gruenbaum

3
@BenjaminGruenbaum - Mein Punkt ist, dass es absolut nichts Falsches ist, wenn sich die Funktion selbst aufruft, um die nächste Iteration zu starten. Es hat keinen Nachteil und ist in der Tat eine effiziente Möglichkeit, asynchrone Operationen zu sequenzieren. Es gibt also keinen Grund, etwas zu vermeiden, das wie eine Rekursion aussieht. Es gibt rekursive Lösungen für einige Probleme, die ineffizient sind - dies ist keine davon.
jfriend00

1
Hey, gemäß einer Diskussion und Anfrage im JavaScript-Raum habe ich diese Antwort bearbeitet, damit wir andere als Kanoniker darauf hinweisen können. Wenn Sie nicht einverstanden sind, lassen Sie es mich bitte wissen und ich werde es wiederherstellen und ein separates öffnen.
Benjamin Gruenbaum

Antworten:


337

Update 2017 : Ich würde eine asynchrone Funktion verwenden, wenn die Umgebung dies unterstützt:

async function readFiles(files) {
  for(const file of files) {
    await readFile(file);
  }
};

Wenn Sie möchten, können Sie das Lesen der Dateien verschieben, bis Sie sie mithilfe eines Async-Generators benötigen (sofern Ihre Umgebung dies unterstützt):

async function* readFiles(files) {
  for(const file of files) {
    yield await readFile(file);
  }
};

Update: Im zweiten Gedanken - ich könnte stattdessen eine for-Schleife verwenden:

var readFiles = function(files) {
  var p = Promise.resolve(); // Q() in q

  files.forEach(file =>
      p = p.then(() => readFile(file)); 
  );
  return p;
};

Oder kompakter mit reduzieren:

var readFiles = function(files) {
  return files.reduce((p, file) => {
     return p.then(() => readFile(file));
  }, Promise.resolve()); // initial
};

In anderen Versprechensbibliotheken (wie when und Bluebird) haben Sie Dienstprogrammmethoden dafür.

Zum Beispiel wäre Bluebird:

var Promise = require("bluebird");
var fs = Promise.promisifyAll(require("fs"));

var readAll = Promise.resolve(files).map(fs.readFileAsync,{concurrency: 1 });
// if the order matters, you can use Promise.each instead and omit concurrency param

readAll.then(function(allFileContents){
    // do stuff to read files.
});

Obwohl es wirklich keinen Grund gibt , Async nicht zu verwenden, warten Sie heute.


2
@ EmreTapcı, nein. Das "=>" einer Pfeilfunktion impliziert bereits die Rückkehr.
Max

Wenn Sie TypeScript verwenden, ist die "for in" -Schleifenlösung meiner Meinung nach am besten. Rückgabe reduzieren rekursive Versprechen z. Der erste Rückgabetyp für Anrufe ist Promise <void>, dann Promise <Promise <void>> und so weiter - es ist unmöglich zu tippen, ohne einen zu verwenden, denke ich
Artur Tagisow

@ArturTagisow TypeScript (zumindest neue Versionen) hat rekursive Typen und sollte die Typen hier korrekt auflösen. Es gibt kein Versprechen <Versprechen <T >>, da Versprechen "rekursiv assimilieren". Promise.resolve(Promise.resolve(15))ist identisch mit Promise.resolve(15).
Benjamin Gruenbaum


72

So führe ich Aufgaben lieber in Serie aus.

function runSerial() {
    var that = this;
    // task1 is a function that returns a promise (and immediately starts executing)
    // task2 is a function that returns a promise (and immediately starts executing)
    return Promise.resolve()
        .then(function() {
            return that.task1();
        })
        .then(function() {
            return that.task2();
        })
        .then(function() {
            console.log(" ---- done ----");
        });
}

Was ist mit Fällen mit mehr Aufgaben? Wie 10?

function runSerial(tasks) {
  var result = Promise.resolve();
  tasks.forEach(task => {
    result = result.then(() => task());
  });
  return result;
}

8
Und was ist mit Fällen, in denen Sie die genaue Anzahl der Aufgaben nicht kennen?
Verdammter

1
Und was ist, wenn Sie die Anzahl der Aufgaben kennen, aber nur zur Laufzeit?
Joeytwiddle

10
"Sie möchten überhaupt nicht über eine Reihe von Versprechungen hinwegarbeiten. Gemäß der Versprechensspezifikation beginnt die Ausführung eines Versprechens, sobald es erstellt wurde. Was Sie also wirklich wollen, ist eine Reihe von Versprechungsfabriken", siehe Erweiterter Fehler Nr. 3 hier: pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
edelans

5
Wenn Sie das Leitungsrauschen reduzieren möchten, können Sie auch schreibenresult = result.then(task);
Daniel Buckmaster

1
@DanielBuckmaster ja, aber seien Sie vorsichtig, denn wenn task () einen Wert zurückgibt, wird dieser an den nächsten Aufruf übergeben. Wenn Ihre Aufgabe optionale Argumente enthält, kann dies zu Nebenwirkungen führen. Der aktuelle Code verschluckt die Ergebnisse und ruft explizit die nächste Aufgabe ohne Argumente auf.
JHH

63

Diese Frage ist alt, aber wir leben in einer Welt mit ES6 und funktionalem JavaScript. Lassen Sie uns sehen, wie wir uns verbessern können.

Da Versprechen sofort ausgeführt werden, können wir nicht einfach eine Reihe von Versprechen erstellen, sie werden alle parallel ausgelöst.

Stattdessen müssen wir eine Reihe von Funktionen erstellen, die ein Versprechen zurückgeben. Jede Funktion wird dann nacheinander ausgeführt, wodurch das Versprechen im Inneren gestartet wird.

Wir können dies auf einige Arten lösen, aber meine Lieblingsmethode ist die Verwendung reduce .

reduceIn Kombination mit Versprechungen wird es etwas schwierig, es zu verwenden , deshalb habe ich den einen Liner unten in einige kleinere verdauliche Bissen zerlegt.

Die Essenz dieser Funktion besteht darin, reducemit einem Anfangswert von Promise.resolve([])oder einem Versprechen zu beginnen, das ein leeres Array enthält.

Dieses Versprechen wird dann als an die reduceMethode weitergegeben promise. Dies ist der Schlüssel, um jedes Versprechen nacheinander zu verketten. Das nächste Versprechen, das ausgeführt werden muss, ist, funcund wenn die thenBrände ausgelöst werden, werden die Ergebnisse verkettet, und dieses Versprechen wird dann zurückgegeben, wobei das ausgeführt wirdreduce Zyklus mit der nächsten Versprechensfunktion ausgeführt wird.

Sobald alle Versprechen ausgeführt wurden, enthält das zurückgegebene Versprechen eine Reihe aller Ergebnisse jedes Versprechens.

ES6 Beispiel (ein Liner)

/*
 * serial executes Promises sequentially.
 * @param {funcs} An array of funcs that return promises.
 * @example
 * const urls = ['/url1', '/url2', '/url3']
 * serial(urls.map(url => () => $.ajax(url)))
 *     .then(console.log.bind(console))
 */
const serial = funcs =>
    funcs.reduce((promise, func) =>
        promise.then(result => func().then(Array.prototype.concat.bind(result))), Promise.resolve([]))

ES6 Beispiel (aufgeschlüsselt)

// broken down to for easier understanding

const concat = list => Array.prototype.concat.bind(list)
const promiseConcat = f => x => f().then(concat(x))
const promiseReduce = (acc, x) => acc.then(promiseConcat(x))
/*
 * serial executes Promises sequentially.
 * @param {funcs} An array of funcs that return promises.
 * @example
 * const urls = ['/url1', '/url2', '/url3']
 * serial(urls.map(url => () => $.ajax(url)))
 *     .then(console.log.bind(console))
 */
const serial = funcs => funcs.reduce(promiseReduce, Promise.resolve([]))

Verwendungszweck:

// first take your work
const urls = ['/url1', '/url2', '/url3', '/url4']

// next convert each item to a function that returns a promise
const funcs = urls.map(url => () => $.ajax(url))

// execute them serially
serial(funcs)
    .then(console.log.bind(console))

1
Sehr gut, danke, Array.prototype.concat.bind(result)ist der Teil, den ich vermisst habe, musste manuell auf Ergebnisse
pushen,

Da es bei uns nur um modernes JS geht, glaube ich, dass die console.log.bind(console)Aussage in Ihrem letzten Beispiel jetzt normalerweise unnötig ist. In diesen Tagen können Sie einfach passieren console.log. Z.B. serial(funcs).then(console.log). Getestet auf aktuellen NodeJS und Chrome.
Molomby

Es war ein wenig schwierig, meinen Kopf herumzuwickeln, aber die Reduzierung macht das im Wesentlichen richtig? Promise.resolve([]).then((x) => { const data = mockApi('/data/1'); return Promise.resolve(x.concat(data)) }).then((x) => { const data = mockApi('/data/2'); return Promise.resolve(x.concat(data)); });
Danecando

@ Danecando, ja das sieht richtig aus. Sie können Promise.resolve auch in der Rückgabe löschen. Alle zurückgegebenen Werte werden automatisch aufgelöst, es sei denn, Sie rufen Promise.reject auf.
Joelnet

@joelnet, als Antwort auf Danecandos Kommentar denke ich, was die Reduzierung tun sollte, sollte im folgenden Ausdruck korrekter ausgedrückt werden, stimmst du zu? Promise.resolve([]).then(x => someApiCall('url1').then(r => x.concat(r))).then(x => someApiCall('url2').then(r => x.concat(r)))und so weiter
bufferoverflow76

37

Um dies einfach in ES6 zu tun:

function(files) {
  // Create a new empty promise (don't do that with real people ;)
  var sequence = Promise.resolve();

  // Loop over each file, and add on a promise to the
  // end of the 'sequence' promise.
  files.forEach(file => {

    // Chain one computation onto the sequence
    sequence = 
      sequence
        .then(() => performComputation(file))
        .then(result => doSomething(result)); 
        // Resolves for each file, one at a time.

  })

  // This will resolve after the entire chain is resolved
  return sequence;
}

1
Scheint, es verwendet Unterstrich. Sie können vereinfachen, files.forEachob Dateien ein Array sind.
Gustavo Rodrigues

2
Nun ... es ist ES5. Der ES6-Weg wäre for (file of files) {...}.
Gustavo Rodrigues

1
Sie sagen, dass Sie nicht verwenden sollten Promise.resolve(), um ein bereits gelöstes Versprechen im wirklichen Leben zu erstellen. Warum nicht? Promise.resolve()scheint sauberer als new Promise(success => success()).
Canac

8
@canac Sorry, es war nur ein Witz mit einem Wortspiel ("leere Versprechen .."). Auf jeden Fall Promise.resolve();in Ihrem Code verwenden.
Shridhar Gupta

1
Schöne Lösung, leicht zu folgen. Ich habe meine nicht in eine Funktion eingeschlossen, um sie am Ende zu lösen, anstatt sie zu return sequence;setzensequence.then(() => { do stuff });
Joe Coyle

25

Einfache Verwendung für das Versprechen von Standard Node.j:

function sequence(tasks, fn) {
    return tasks.reduce((promise, task) => promise.then(() => fn(task)), Promise.resolve());
}

AKTUALISIEREN

items-versprechen ist ein gebrauchsfertiges NPM-Paket, das dasselbe tut.


6
Ich würde gerne sehen, wie dies ausführlicher erklärt wird.
Tyguy7

Ich habe eine Variation dieser Antwort mit Erläuterungen unten angegeben. Vielen Dank
Sarsaparilla

Dies ist genau das, was ich in Umgebungen vor Node 7 mache, die keinen Zugriff auf async / await haben. Schön und sauber.
JHH

11

Ich musste viele sequentielle Aufgaben ausführen und habe diese Antworten verwendet, um eine Funktion zu erstellen, die sich um die Bearbeitung jeder sequentiellen Aufgabe kümmert ...

function one_by_one(objects_array, iterator, callback) {
    var start_promise = objects_array.reduce(function (prom, object) {
        return prom.then(function () {
            return iterator(object);
        });
    }, Promise.resolve()); // initial
    if(callback){
        start_promise.then(callback);
    }else{
        return start_promise;
    }
}

Die Funktion akzeptiert 2 Argumente + 1 optional. Das erste Argument ist das Array, an dem wir arbeiten werden. Das zweite Argument ist die Aufgabe selbst, eine Funktion, die ein Versprechen zurückgibt. Die nächste Aufgabe wird erst gestartet, wenn dieses Versprechen aufgelöst wird. Das dritte Argument ist ein Rückruf, der ausgeführt wird, wenn alle Aufgaben erledigt wurden. Wenn kein Rückruf übergeben wird, gibt die Funktion das von ihr erstellte Versprechen zurück, damit wir das Ende bewältigen können.

Hier ist ein Anwendungsbeispiel:

var filenames = ['1.jpg','2.jpg','3.jpg'];
var resize_task = function(filename){
    //return promise of async resizing with filename
};
one_by_one(filenames,resize_task );

Hoffe es spart jemandem etwas Zeit ...


Unglaubliche Lösung, es war die beste, die ich in fast einer Woche des Kämpfens gefunden habe ... Es ist sehr gut erklärt, hat logische innere Namen, ein gutes Beispiel (könnte besser sein), ich kann es sicher so viele fordern Zeiten nach Bedarf, und es enthält die Option, Rückrufe zu setzen. einfach schön! (Ich habe gerade den Namen in etwas geändert, das mir mehr Sinn macht.) .... EMPFEHLUNG für andere ... Sie können ein Objekt mit 'Object.keys ( myObject )' als Ihr 'object_array'
iterieren

Vielen Dank für Ihren Kommentar! Ich benutze diesen Namen auch nicht, aber ich wollte ihn hier offensichtlicher / einfacher machen.
Salketer

5

Die schönste Lösung, die ich herausfinden konnte, war mit bluebirdVersprechungen. Sie können einfach tun, Promise.resolve(files).each(fs.readFileAsync);was garantiert, dass Versprechen nacheinander gelöst werden.


1
Noch besser : Promise.each(filtes, fs.readFileAsync). Übrigens, musst du das nicht tun .bind(fs)?
Bergi

Niemand hier scheint den Unterschied zwischen einem Array und einer Sequenz zu verstehen, dass letztere eine unbegrenzte / dynamische Größe impliziert.
Vitaly-t

Beachten Sie, dass Arrays in Javascript nichts mit Arrays fester Größe in Sprachen im C-Stil zu tun haben. Sie sind nur Objekte mit angeschraubter numerischer Schlüsselverwaltung und haben keine vorgeschriebene Größe oder Begrenzung ( insbesondere nicht bei Verwendung new Array(int). Sie müssen lediglich das lengthSchlüssel-Wert-Paar voreingestellt haben, was sich darauf auswirkt, wie viele Indizes während der längenbasierten Iteration verwendet werden. Es hat Null Auswirkung auf die Indexierung oder
Indexgrenzen

4

Dies ist eine geringfügige Abweichung von einer anderen Antwort oben. Native Versprechen verwenden:

function inSequence(tasks) {
    return tasks.reduce((p, task) => p.then(task), Promise.resolve())
}

Erläuterung

Wenn Sie diese Aufgaben haben [t1, t2, t3], entspricht das oben Gesagte Promise.resolve().then(t1).then(t2).then(t3). Es ist das Verhalten von reduzieren.

Wie benutzt man

Zuerst müssen Sie eine Liste von Aufgaben erstellen! Eine Aufgabe ist eine Funktion, die kein Argument akzeptiert. Wenn Sie Argumente an Ihre Funktion übergeben müssen, verwenden Sie bindoder andere Methoden, um eine Aufgabe zu erstellen. Zum Beispiel:

var tasks = files.map(file => processFile.bind(null, file))
inSequence(tasks).then(...)

4

Meine bevorzugte Lösung:

function processArray(arr, fn) {
    return arr.reduce(
        (p, v) => p.then((a) => fn(v).then(r => a.concat([r]))),
        Promise.resolve([])
    );
}

Es unterscheidet sich nicht grundlegend von anderen hier veröffentlichten, aber:

  • Wendet die Funktion auf Elemente in Serie an
  • Wird in eine Reihe von Ergebnissen aufgelöst
  • Erfordert kein Async / Warten (Support ist immer noch recht begrenzt, circa 2017)
  • Verwendet Pfeilfunktionen; nett und prägnant

Anwendungsbeispiel:

const numbers = [0, 4, 20, 100];
const multiplyBy3 = (x) => new Promise(res => res(x * 3));

// Prints [ 0, 12, 60, 300 ]
processArray(numbers, multiplyBy3).then(console.log);

Getestet auf angemessenem aktuellem Chrome (v59) und NodeJS (v8.1.2).


3

Verwenden Sie Array.prototype.reduceund denken Sie daran, Ihre Versprechen in eine Funktion zu verpacken, da sie sonst bereits ausgeführt werden!

// array of Promise providers

const providers = [
  function(){
     return Promise.resolve(1);
  },
  function(){
     return Promise.resolve(2);
  },
  function(){
     return Promise.resolve(3);
  }
]


const inSeries = function(providers){

  const seed = Promise.resolve(null); 

  return providers.reduce(function(a,b){
      return a.then(b);
  }, seed);
};

nett und einfach ... Sie sollten in der Lage sein, den gleichen Samen für die Leistung usw. wiederzuverwenden.

Es ist wichtig, sich vor leeren Arrays oder Arrays mit nur 1 Element zu schützen, wenn Sie Reduzieren verwenden. Daher ist diese Technik die beste Wahl:

   const providers = [
      function(v){
         return Promise.resolve(v+1);
      },
      function(v){
         return Promise.resolve(v+2);
      },
      function(v){
         return Promise.resolve(v+3);
      }
    ]

    const inSeries = function(providers, initialVal){

        if(providers.length < 1){
            return Promise.resolve(null)
        }

        return providers.reduce((a,b) => a.then(b), providers.shift()(initialVal));
    };

und dann nenne es wie:

inSeries(providers, 1).then(v => {
   console.log(v);  // 7
});

2

Ich habe diese einfache Methode für das Promise-Objekt erstellt:

Erstellen Sie eine Promise.sequence-Methode und fügen Sie sie dem Promise-Objekt hinzu

Promise.sequence = function (chain) {
    var results = [];
    var entries = chain;
    if (entries.entries) entries = entries.entries();
    return new Promise(function (yes, no) {
        var next = function () {
            var entry = entries.next();
            if(entry.done) yes(results);
            else {
                results.push(entry.value[1]().then(next, function() { no(results); } ));
            }
        };
        next();
    });
};

Verwendungszweck:

var todo = [];

todo.push(firstPromise);
if (someCriterium) todo.push(optionalPromise);
todo.push(lastPromise);

// Invoking them
Promise.sequence(todo)
    .then(function(results) {}, function(results) {});

Das Beste an dieser Erweiterung des Promise-Objekts ist, dass sie mit dem Stil der Versprechen übereinstimmt. Promise.all und Promise.sequence werden auf dieselbe Weise aufgerufen, haben jedoch unterschiedliche Semantiken.

Vorsicht

Das sequentielle Ausführen von Versprechungen ist normalerweise keine sehr gute Möglichkeit, Versprechungen zu verwenden. Normalerweise ist es besser, Promise.all zu verwenden und den Browser den Code so schnell wie möglich ausführen zu lassen. Es gibt jedoch echte Anwendungsfälle dafür - zum Beispiel beim Schreiben einer mobilen App mit Javascript.


Nein, Sie können nicht vergleichen Promise.allund Ihre Promise.sequence. Einer nimmt eine Reihe von Versprechungen, der andere eine Reihe von Funktionen, die Versprechungen zurückgeben.
Bergi

Übrigens würde ich empfehlen, das Versprechen Konstruktor Antipattern
Bergi

Wusste nicht, dass es einen Iterator brauchte. Sollte aber einfach genug sein, um es umzuschreiben. Könnten Sie näher erläutern, warum dies das Antipattern des Versprechenskonstruktors ist? Ich habe Ihren Beitrag hier gelesen: stackoverflow.com/a/25569299/1667011
frodeborli

@Bergi Ich habe den Code aktualisiert, um Iteratoren zu unterstützen. Ich sehe immer noch nicht, dass dies ein Antimuster ist. Antipatterns sind im Allgemeinen als Richtlinien zu betrachten, um Codierungsfehler zu vermeiden, und es ist absolut gültig, (Bibliotheks-) Funktionen zu erstellen, die gegen diese Richtlinien verstoßen.
Frodeborli

Ja, wenn Sie es als Bibliotheksfunktion betrachten, ist es in Ordnung, aber in diesem Fall ist ein reduceähnliches wie in Benjamins Antwort viel einfacher.
Bergi

2

Sie können diese Funktion verwenden, mit der die Liste der versprochenen Fakten abgerufen wird:

function executeSequentially(promiseFactories) {
    var result = Promise.resolve();
    promiseFactories.forEach(function (promiseFactory) {
        result = result.then(promiseFactory);
    });
    return result;
}

Promise Factory ist nur eine einfache Funktion, die ein Versprechen zurückgibt:

function myPromiseFactory() {
    return somethingThatCreatesAPromise();
}

Es funktioniert, weil eine Versprechensfabrik das Versprechen erst erstellt, wenn sie dazu aufgefordert wird. Es funktioniert genauso wie eine damalige Funktion - tatsächlich ist es dasselbe!

Sie möchten überhaupt nicht über eine Reihe von Versprechungen hinweggehen. Gemäß der Versprechen-Spezifikation beginnt die Ausführung eines Versprechens, sobald es erstellt wurde. Was Sie also wirklich wollen, ist eine Reihe von Versprechungsfabriken ...

Wenn Sie mehr über Versprechen erfahren möchten, sollten Sie diesen Link überprüfen: https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html


2

Meine Antwort basiert auf https://stackoverflow.com/a/31070150/7542429 .

Promise.series = function series(arrayOfPromises) {
    var results = [];
    return arrayOfPromises.reduce(function(seriesPromise, promise) {
      return seriesPromise.then(function() {
        return promise
        .then(function(result) {
          results.push(result);
        });
      });
    }, Promise.resolve())
    .then(function() {
      return results;
    });
  };

Diese Lösung gibt die Ergebnisse als Array wie Promise.all () zurück.

Verwendungszweck:

Promise.series([array of promises])
.then(function(results) { 
  // do stuff with results here
});

2

Die Antwort von @ joelnet hat mir sehr gut gefallen, aber für mich ist diese Art der Codierung etwas schwer zu verdauen. Deshalb habe ich ein paar Tage lang versucht, herauszufinden, wie ich dieselbe Lösung besser lesbar ausdrücken kann, und das ist meine nehmen, nur mit einer anderen Syntax und einigen Kommentaren.

// first take your work
const urls = ['/url1', '/url2', '/url3', '/url4']

// next convert each item to a function that returns a promise
const functions = urls.map((url) => {
  // For every url we return a new function
  return () => {
    return new Promise((resolve) => {
      // random wait in milliseconds
      const randomWait = parseInt((Math.random() * 1000),10)
      console.log('waiting to resolve in ms', randomWait)
      setTimeout(()=>resolve({randomWait, url}),randomWait)
    })
  }
})


const promiseReduce = (acc, next) => {
  // we wait for the accumulator to resolve it's promise
  return acc.then((accResult) => {
    // and then we return a new promise that will become
    // the new value for the accumulator
    return next().then((nextResult) => {
      // that eventually will resolve to a new array containing
      // the value of the two promises
      return accResult.concat(nextResult)
    })
  })
};
// the accumulator will always be a promise that resolves to an array
const accumulator = Promise.resolve([])

// we call reduce with the reduce function and the accumulator initial value
functions.reduce(promiseReduce, accumulator)
  .then((result) => {
    // let's display the final value here
    console.log('=== The final result ===')
    console.log(result)
  })

2

Wie Bergi bemerkte, denke ich, dass die beste und klarste Lösung die Verwendung von BlueBird.each ist, Code unten:

const BlueBird = require('bluebird');
BlueBird.each(files, fs.readFileAsync);

2

Zunächst müssen Sie verstehen, dass ein Versprechen zum Zeitpunkt der Erstellung ausgeführt wird.
Also zum Beispiel, wenn Sie einen Code haben:

["a","b","c"].map(x => returnsPromise(x))

Sie müssen es ändern in:

["a","b","c"].map(x => () => returnsPromise(x))

Dann müssen wir Versprechen nacheinander verketten:

["a", "b", "c"].map(x => () => returnsPromise(x))
    .reduce(
        (before, after) => before.then(_ => after()),
        Promise.resolve()
    )

Durch Ausführen after()wird sichergestellt, dass das Versprechen nur dann erstellt (und ausgeführt) wird, wenn es soweit ist.


1

Ich verwende den folgenden Code, um das Promise-Objekt zu erweitern. Es behandelt die Ablehnung der Versprechen und gibt eine Reihe von Ergebnissen zurück

Code

/*
    Runs tasks in sequence and resolves a promise upon finish

    tasks: an array of functions that return a promise upon call.
    parameters: an array of arrays corresponding to the parameters to be passed on each function call.
    context: Object to use as context to call each function. (The 'this' keyword that may be used inside the function definition)
*/
Promise.sequence = function(tasks, parameters = [], context = null) {
    return new Promise((resolve, reject)=>{

        var nextTask = tasks.splice(0,1)[0].apply(context, parameters[0]); //Dequeue and call the first task
        var output = new Array(tasks.length + 1);
        var errorFlag = false;

        tasks.forEach((task, index) => {
            nextTask = nextTask.then(r => {
                output[index] = r;
                return task.apply(context, parameters[index+1]);
            }, e=>{
                output[index] = e;
                errorFlag = true;
                return task.apply(context, parameters[index+1]);
            });
        });

        // Last task
        nextTask.then(r=>{
            output[output.length - 1] = r;
            if (errorFlag) reject(output); else resolve(output);
        })
        .catch(e=>{
            output[output.length - 1] = e;
            reject(output);
        });
    });
};

Beispiel

function functionThatReturnsAPromise(n) {
    return new Promise((resolve, reject)=>{
        //Emulating real life delays, like a web request
        setTimeout(()=>{
            resolve(n);
        }, 1000);
    });
}

var arrayOfArguments = [['a'],['b'],['c'],['d']];
var arrayOfFunctions = (new Array(4)).fill(functionThatReturnsAPromise);


Promise.sequence(arrayOfFunctions, arrayOfArguments)
.then(console.log)
.catch(console.error);

1

Wenn Sie möchten, können Sie Reduzieren verwenden, um ein sequentielles Versprechen abzugeben, zum Beispiel:

[2,3,4,5,6,7,8,9].reduce((promises, page) => {
    return promises.then((page) => {
        console.log(page);
        return Promise.resolve(page+1);
    });
  }, Promise.resolve(1));

es wird immer nacheinander funktionieren.


1

Mit modernem ES:

const series = async (tasks) => {
  const results = [];

  for (const task of tasks) {
    const result = await task;

    results.push(result);
  }

  return results;
};

//...

const readFiles = await series(files.map(readFile));

1

Mit Async / Await (wenn Sie die Unterstützung von ES7 haben)

function downloadFile(fileUrl) { ... } // This function return a Promise

async function main()
{
  var filesList = [...];

  for (const file of filesList) {
    await downloadFile(file);
  }
}

(Sie müssen forloop verwenden und nicht, forEachweil async / await Probleme beim Ausführen einer forEach-Schleife hat.)

Ohne Async / Warten (mit Promise)

function downloadFile(fileUrl) { ... } // This function return a Promise

function downloadRecursion(filesList, index)
{
  index = index || 0;
  if (index < filesList.length)
  {
    downloadFile(filesList[index]).then(function()
    {
      index++;
      downloadRecursion(filesList, index); // self invocation - recursion!
    });
  }
  else
  {
    return Promise.resolve();
  }
}

function main()
{
  var filesList = [...];
  downloadRecursion(filesList);
}

2
Warten Sie auf jeden Fall wird nicht empfohlen.
Marcelo Agimóvel

@ MarceloAgimóvel - Ich habe auf Lösung aktualisiert, um nicht mit forEach(gemäß dieser ) zu arbeiten
Gil Epshtain

0

Anhand des Titels der Frage "Versprechen nacheinander auflösen (dh nacheinander)?" Könnten wir verstehen, dass das OP mehr an der sequentiellen Behandlung von Versprechungen bei der Abwicklung als an sequentiellen Anrufen an sich interessiert ist .

Diese Antwort wird angeboten:

  • um zu demonstrieren, dass sequentielle Aufrufe für die sequentielle Verarbeitung von Antworten nicht erforderlich sind.
  • den Besuchern dieser Seite tragfähige alternative Muster aufzuzeigen - einschließlich des OP, wenn er über ein Jahr später noch interessiert ist.
  • trotz der Behauptung des OP, dass er nicht gleichzeitig telefonieren möchte, was tatsächlich der Fall sein kann, aber ebenso eine Annahme sein kann, die auf dem Wunsch nach sequentieller Behandlung von Antworten beruht, wie der Titel impliziert.

Wenn gleichzeitige Anrufe wirklich nicht erwünscht sind, lesen Sie die Antwort von Benjamin Gruenbaum, in der sequentielle Anrufe (usw.) umfassend behandelt werden.

Wenn Sie jedoch (für eine verbesserte Leistung) an Mustern interessiert sind, die gleichzeitige Anrufe und anschließende sequentielle Bearbeitung von Antworten ermöglichen, lesen Sie bitte weiter.

Es ist verlockend zu glauben, dass Sie Promise.all(arr.map(fn)).then(fn)(wie ich es schon oft getan habe) oder den ausgefallenen Zucker einer Promise lib (insbesondere den von Bluebird) verwenden müssen, aber (mit Anerkennung dieses Artikels ) ein arr.map(fn).reduce(fn)Muster wird die Arbeit erledigen, mit den Vorteilen, dass es:

  • funktioniert nur mit jeder Versprechen-Bibliothek - auch mit vorkonformen Versionen von jQuery .then() verwendet.
  • bietet die Flexibilität, Fehler zu überspringen oder Fehler zu stoppen, je nachdem, was Sie mit einem einzeiligen Mod möchten.

Hier ist es geschrieben für Q.

var readFiles = function(files) {
    return files.map(readFile) //Make calls in parallel.
    .reduce(function(sequence, filePromise) {
        return sequence.then(function() {
            return filePromise;
        }).then(function(file) {
            //Do stuff with file ... in the correct sequence!
        }, function(error) {
            console.log(error); //optional
            return sequence;//skip-over-error. To stop-on-error, `return error` (jQuery), or `throw  error` (Promises/A+).
        });
    }, Q()).then(function() {
        // all done.
    });
};

Hinweis: Nur dieses eine Fragment Q()ist spezifisch für Q. Für jQuery müssen Sie sicherstellen, dass readFile () ein jQuery-Versprechen zurückgibt. Mit A + libs werden ausländische Versprechen aufgenommen.

Der Schlüssel hier ist das sequenceVersprechen der Reduktion , das den Umgang mit den readFileVersprechen, aber nicht deren Erstellung , in eine Reihenfolge bringt.

Und wenn Sie das einmal aufgenommen haben, ist es vielleicht etwas umwerfend, wenn Sie feststellen, dass die .map()Bühne eigentlich nicht notwendig ist! Der gesamte Auftrag, parallele Anrufe plus serielle Bearbeitung in der richtigen Reihenfolge, kann reduce()allein erledigt werden, plus dem zusätzlichen Vorteil einer weiteren Flexibilität für:

  • Konvertieren Sie von parallelen asynchronen Aufrufen zu seriellen asynchronen Aufrufen, indem Sie einfach eine Zeile verschieben - möglicherweise nützlich während der Entwicklung.

Hier ist es Qwieder.

var readFiles = function(files) {
    return files.reduce(function(sequence, f) {
        var filePromise = readFile(f);//Make calls in parallel. To call sequentially, move this line down one.
        return sequence.then(function() {
            return filePromise;
        }).then(function(file) {
            //Do stuff with file ... in the correct sequence!
        }, function(error) {
            console.log(error); //optional
            return sequence;//Skip over any errors. To stop-on-error, `return error` (jQuery), or `throw  error` (Promises/A+).
        });
    }, Q()).then(function() {
        // all done.
    });
};

Das ist das Grundmuster. Wenn Sie dem Anrufer auch Daten (z. B. die Dateien oder eine Transformation davon) liefern möchten, benötigen Sie eine milde Variante.


Ich denke nicht, dass es eine gute Idee ist, Fragen zu beantworten, die den Absichten der OP widersprechen…
Bergi

1
Dieses sequence.then(() => filePromise)Ding ist ein Antimuster - es verbreitet Fehler nicht so schnell wie möglich (und erstellt unhandledRejectionin Bibliotheken, die sie unterstützen). Sie sollten lieber Q.all([sequence, filePromise])oder verwenden $.when(sequence, filePromise). Zugegeben, dieses Verhalten könnte das sein, was Sie wollen, wenn Sie Fehler ignorieren oder überspringen möchten, aber Sie sollten dies zumindest als Nachteil erwähnen.
Bergi

@Bergi, ich hoffe, dass das OP eingreift und beurteilt, ob dies wirklich seinen Absichten widerspricht oder nicht. Wenn nicht, werde ich die Antwort löschen, während ich hoffe, dass ich meine Position begründet habe. Vielen Dank, dass Sie es ernst genug genommen haben, um ein anständiges Feedback zu geben. Können Sie mehr über das Anti-Pattern erklären oder bitte eine Referenz angeben? Gilt das auch für den Artikel, in dem ich das Grundmuster gefunden habe ?
Roamer-1888

1
Ja, die dritte Version seines Codes (dh "sowohl parallel als auch sequentiell") hat das gleiche Problem. Das "Antipattern" erfordert eine ausgefeilte Fehlerbehandlung und neigt dazu, Handler asynchron anzuhängen, was zu unhandledRejectionEreignissen führt. In Bluebird können Sie dies umgehen, indem Sie sequence.return(filePromise)das gleiche Verhalten verwenden, aber Ablehnungen problemlos verarbeiten. Ich kenne keine Referenz, ich habe sie mir gerade ausgedacht - ich glaube, das "(Anti) Muster" hat noch keinen Namen.
Bergi

1
@Bergi, Sie können deutlich etwas sehen, das ich nicht sehen kann :( Ich frage mich, ob dieses neue Anti-Muster irgendwo dokumentiert werden muss?
Roamer-1888

0

Ihr Ansatz ist nicht schlecht, hat jedoch zwei Probleme: Er verschluckt Fehler und verwendet das Explicit Promise Construction Antipattern.

Sie können beide Probleme lösen und den Code sauberer gestalten, während Sie immer noch dieselbe allgemeine Strategie anwenden:

var Q = require("q");

var readFile = function(file) {
  ... // Returns a promise.
};

var readFiles = function(files) {
  var readSequential = function(index) {
    if (index < files.length) {
      return readFile(files[index]).then(function() {
        return readSequential(index + 1);
      });
    }
  };

  // using Promise.resolve() here in case files.length is 0
  return Promise.resolve(readSequential(0)); // Start!
};

0

Wenn eine andere Person bei der Ausführung von CRUD-Operationen eine garantierte Methode zur strikt sequentiellen Lösung von Versprechungen benötigt, können Sie auch den folgenden Code als Grundlage verwenden.

Solange Sie 'return' hinzufügen, bevor Sie jede Funktion aufrufen, ein Versprechen beschreiben und dieses Beispiel als Grundlage verwenden, wird der nächste Funktionsaufruf .then () nach Abschluss des vorherigen konsequent gestartet:

getRidOfOlderShoutsPromise = () => {
    return readShoutsPromise('BEFORE')
    .then(() => {
        return deleteOlderShoutsPromise();
    })
    .then(() => {
        return readShoutsPromise('AFTER')
    })
    .catch(err => console.log(err.message));
}

deleteOlderShoutsPromise = () => {
    return new Promise ( (resolve, reject) => {
        console.log("in deleteOlderShouts");
        let d = new Date();
        let TwoMinuteAgo = d - 1000 * 90 ;
        All_Shouts.deleteMany({ dateTime: {$lt: TwoMinuteAgo}}, function(err) {
            if (err) reject();
            console.log("DELETED OLDs at "+d);
            resolve();        
        });
    });
}

readShoutsPromise = (tex) => {
    return new Promise( (resolve, reject) => {
        console.log("in readShoutsPromise -"+tex);
        All_Shouts
        .find({})
        .sort([['dateTime', 'ascending']])
        .exec(function (err, data){
            if (err) reject();
            let d = new Date();
            console.log("shouts "+tex+" delete PROMISE = "+data.length +"; date ="+d);
            resolve(data);
        });    
    });
}

0

Die Array-Push- und Pop-Methode kann für die Abfolge von Versprechungen verwendet werden. Sie können auch neue Versprechen abgeben, wenn Sie zusätzliche Daten benötigen. Dies ist der Code, den ich in React Infinite Loader verwenden werde, um eine Seitenfolge zu laden.

var promises = [Promise.resolve()];

function methodThatReturnsAPromise(page) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			console.log(`Resolve-${page}! ${new Date()} `);
			resolve();
		}, 1000);
	});
}

function pushPromise(page) {
	promises.push(promises.pop().then(function () {
		return methodThatReturnsAPromise(page)
	}));
}

pushPromise(1);
pushPromise(2);
pushPromise(3);


0

Die meisten Antworten enthalten nicht die Ergebnisse ALLER Versprechen einzeln. Wenn also jemand nach diesem bestimmten Verhalten sucht, ist dies eine mögliche Lösung mit Rekursion.

Es folgt dem Stil von Promise.all:

  • Gibt das Ergebnisarray im .then()Rückruf zurück.

  • Wenn ein Versprechen fehlschlägt, wird es sofort im .catch()Rückruf zurückgegeben.

const promiseEach = (arrayOfTasks) => {
  let results = []
  return new Promise((resolve, reject) => {
    const resolveNext = (arrayOfTasks) => {
      // If all tasks are already resolved, return the final array of results
      if (arrayOfTasks.length === 0) return resolve(results)

      // Extract first promise and solve it
      const first = arrayOfTasks.shift()

      first().then((res) => {
        results.push(res)
        resolveNext(arrayOfTasks)
      }).catch((err) => {
        reject(err)
      })
    }
    resolveNext(arrayOfTasks)
  })
}

// Lets try it 😎

const promise = (time, shouldThrowError) => new Promise((resolve, reject) => {
  const timeInMs = time * 1000
  setTimeout(()=>{
    console.log(`Waited ${time} secs`)
    if (shouldThrowError) reject(new Error('Promise failed'))
    resolve(time)
  }, timeInMs)
})

const tasks = [() => promise(1), () => promise(2)]

promiseEach(tasks)
  .then((res) => {
    console.log(res) // [1, 2]
  })
  // Oops some promise failed
  .catch((error) => {
    console.log(error)
  })

Hinweis zur tasksArray-Deklaration :

In diesem Fall ist es nicht möglich, die folgende Notation zu verwenden, wie Promise.allsie verwendet werden würde:

const tasks = [promise(1), promise(2)]

Und wir müssen verwenden:

const tasks = [() => promise(1), () => promise(2)]

Der Grund dafür ist, dass JavaScript das Versprechen sofort nach seiner Deklaration ausführt. Wenn wir Methoden wie verwenden Promise.all, wird nur überprüft, ob der Status aller von ihnen ist fulfilledoder rejected, aber die Exektion selbst wird nicht gestartet. Mit () => promise()stoppen wir die Ausführung, bis sie aufgerufen wird.


0
(function() {
  function sleep(ms) {
    return new Promise(function(resolve) {
      setTimeout(function() {
        return resolve();
      }, ms);
    });
  }

  function serial(arr, index, results) {
    if (index == arr.length) {
      return Promise.resolve(results);
    }
    return new Promise(function(resolve, reject) {
      if (!index) {
        index = 0;
        results = [];
      }
      return arr[index]()
        .then(function(d) {
          return resolve(d);
        })
        .catch(function(err) {
          return reject(err);
        });
    })
      .then(function(result) {
        console.log("here");
        results.push(result);
        return serial(arr, index + 1, results);
      })
      .catch(function(err) {
        throw err;
      });
  }

  const a = [5000, 5000, 5000];

  serial(a.map(x => () => sleep(x)));
})();

Hier ist der Schlüssel, wie Sie die Schlaffunktion aufrufen. Sie müssen ein Array von Funktionen übergeben, das selbst ein Versprechen anstelle eines Arrays von Versprechen zurückgibt.


-1

Dies soll erweitern, wie eine Folge von Versprechungen allgemeiner verarbeitet werden kann, wobei dynamische / unendliche Folgen unterstützt werden, die auf der Implementierung von spex.sequence basieren :

var $q = require("q");
var spex = require('spex')($q);

var files = []; // any dynamic source of files;

var readFile = function (file) {
    // returns a promise;
};

function source(index) {
    if (index < files.length) {
        return readFile(files[index]);
    }
}

function dest(index, data) {
    // data = resolved data from readFile;
}

spex.sequence(source, dest)
    .then(function (data) {
        // finished the sequence;
    })
    .catch(function (error) {
        // error;
    });

Diese Lösung funktioniert nicht nur mit Sequenzen beliebiger Größe, sondern Sie können auch problemlos Datenbeschränkung und Lastausgleich hinzufügen .

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.