Risolvi le promesse una dopo l'altra (cioè in sequenza)?


269

Si consideri il seguente codice che legge una matrice di file in modo seriale / sequenziale. readFilesrestituisce una promessa, che viene risolta solo dopo che tutti i file sono stati letti in sequenza.

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!

  });
};

Il codice sopra funziona, ma non mi piace dover ricorrere per far accadere le cose in sequenza. C'è un modo più semplice in cui questo codice può essere riscritto in modo da non dover usare la mia strana readSequentialfunzione?

Inizialmente ho provato a utilizzare Promise.all, ma ciò ha causato tutte le readFilechiamate contemporaneamente, che non è quello che voglio:

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

2
Tutto ciò che deve attendere il completamento di un'operazione asincrona precedente deve essere eseguito in un callback. L'uso delle promesse non cambia questo. Quindi hai bisogno della ricorsione.
Barmar,

1
Cordiali saluti, questa non è tecnicamente ricorsione in quanto non vi è alcun accumulo di frame dello stack. Il precedente readFileSequential()è già tornato prima che venga chiamato il successivo (poiché è asincrono, si completa molto dopo che la chiamata di funzione originale è già stata restituita).
jfriend00,

1
@ jfriend00 L'accumulo di frame dello stack non è richiesto per la ricorsione - solo riferimento personale. Questo è solo un tecnicismo però.
Benjamin Gruenbaum,

3
@BenjaminGruenbaum - il mio punto è che non c'è assolutamente nulla di sbagliato nell'avere la funzione stessa chiamata per dare il via alla prossima iterazione. C'è zero svantaggio ad esso e, in effetti, è un modo efficiente per sequenziare le operazioni asincrone. Quindi, non c'è motivo di evitare qualcosa che sembra ricorsione. Ci sono soluzioni ricorsive ad alcuni problemi che sono inefficienti - questo non è uno di quelli.
jfriend00,

1
Ehi, per una discussione e una richiesta nella stanza JavaScript ho modificato questa risposta in modo che possiamo indicarne altre come canoniche. Se non sei d'accordo, fammi sapere e lo ripristinerò e ne aprirò uno separato.
Benjamin Gruenbaum,

Risposte:


338

Aggiornamento 2017 : utilizzerei una funzione asincrona se l'ambiente la supporta:

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

Se lo desideri, puoi rinviare la lettura dei file fino a quando non ti servono utilizzando un generatore asincrono (se il tuo ambiente lo supporta):

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

Aggiornamento: ripensandoci, potrei usare invece un ciclo for:

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

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

O più compatto, con riduci:

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

In altre librerie promesse (come quando e Bluebird) hai metodi di utilità per questo.

Ad esempio, Bluebird sarebbe:

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

Anche se non c'è davvero alcun motivo per non usare l'asincronismo oggi.


2
@ EmreTapcı, no. La funzione "=>" di una freccia implica già il ritorno.
Max

Se usi TypeScript, penso che la soluzione "for in" sia la migliore. Ridurre i resi Promesse ricorsive, ad es. il primo tipo di ritorno di chiamata è Promessa <void>, poi il secondo è Promessa <Promessa <void>> e così via - è impossibile digitare senza usare qualsiasi credo
Artur Tagisow

@ArturTagisow TypeScript (almeno nuove versioni) hanno tipi ricorsivi e dovrebbero risolverli correttamente qui. Non esiste una Promessa <Promessa <T>> poiché le promesse "ricorsivamente si assimilano". Promise.resolve(Promise.resolve(15))è identico a Promise.resolve(15).
Benjamin Gruenbaum,


72

Ecco come preferisco eseguire le attività in serie.

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

Che dire dei casi con più compiti? Tipo 10?

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

8
E i casi in cui non si conosce il numero esatto di attività?
maledetto

1
E quando conosci il numero di attività, ma solo in fase di esecuzione?
joeytwiddle,

10
"non vuoi affatto operare su una serie di promesse. Per le specifiche della promessa, non appena viene creata una promessa, inizia l'esecuzione. Quindi quello che vuoi davvero è una serie di fabbriche di promesse" vedi Errore avanzato n. 3 qui: pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
edelans

5
Se ti piace ridurre il rumore di linea, puoi anche scrivereresult = result.then(task);
Daniel Buckmaster,

1
@DanielBuckmaster sì, ma fai attenzione, poiché se task () restituisce un valore, verrà passato alla successiva chiamata. Se l'attività ha argomenti opzionali, ciò potrebbe causare effetti collaterali. Il codice corrente ingoia i risultati e richiama esplicitamente l'attività successiva senza argomenti.
JHH,

63

Questa domanda è vecchia, ma viviamo in un mondo di ES6 e JavaScript funzionale, quindi vediamo come possiamo migliorare.

Poiché le promesse vengono eseguite immediatamente, non possiamo semplicemente creare una serie di promesse, che si attiverebbero tutte in parallelo.

Invece, dobbiamo creare una serie di funzioni che restituiscono una promessa. Ogni funzione verrà quindi eseguita in sequenza, il che avvia la promessa all'interno.

Possiamo risolverlo in alcuni modi, ma il mio modo preferito è usare reduce.

Diventa un po 'complicato da usare reducein combinazione con le promesse, quindi ho suddiviso la fodera in alcuni piccoli morsi digeribili di seguito.

L'essenza di questa funzione è utilizzare a reducepartire da un valore iniziale di Promise.resolve([]), o una promessa contenente un array vuoto.

Questa promessa verrà quindi passata al reducemetodo come promise. Questa è la chiave per concatenare ogni promessa insieme in sequenza. La prossima promessa da eseguire è funce quando gli thenincendi, i risultati vengono concatenati e quella promessa viene quindi restituita, eseguendo il reduceciclo con la successiva funzione di promessa.

Una volta eseguite tutte le promesse, la promessa restituita conterrà una serie di tutti i risultati di ciascuna promessa.

Esempio ES6 (un 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([]))

Esempio ES6 (suddiviso)

// 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([]))

Uso:

// 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
molto bene, grazie, Array.prototype.concat.bind(result)è la parte che mi mancava, ho dovuto fare spingendo manualmente ai risultati che hanno funzionato ma è stato meno bello
zavr

Dal momento che siamo tutti incentrati sul moderno JS, credo che la console.log.bind(console)frase del tuo ultimo esempio non sia di solito necessaria. In questi giorni puoi semplicemente passare console.log. Per esempio. serial(funcs).then(console.log). Testato su nodejs e Chrome correnti.
Molomby,

È stato un po 'difficile avvolgere la testa, ma il riduttore sta sostanzialmente facendo questo? 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, sì, questo sembra corretto. Puoi anche rilasciare Promise.resolve nel ritorno, tutti i valori restituiti verranno automaticamente risolti a meno che tu non chiami Promise.reject su di essi.
joelnet,

@joelnet, in risposta al commento di Danecando, penso che cosa la riduzione dovrebbe essere più corretta espressa nella seguente espressione, sei d'accordo? Promise.resolve([]).then(x => someApiCall('url1').then(r => x.concat(r))).then(x => someApiCall('url2').then(r => x.concat(r)))e così via
bufferoverflow76

37

Per fare questo semplicemente in ES6:

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
Sembra che usi il trattino basso. È possibile semplificare files.forEachse i file sono un array.
Gustavo Rodrigues,

2
Bene ... è ES5. Il modo ES6 sarebbe for (file of files) {...}.
Gustavo Rodrigues

1
Dici che non dovresti usare Promise.resolve()per creare una promessa già risolta nella vita reale. Perchè no? Promise.resolve()sembra più pulito di new Promise(success => success()).
canac

8
@canac Siamo spiacenti, era solo uno scherzo con un gioco di parole ("promesse vuote .."). Sicuramente utilizzare Promise.resolve();nel tuo codice.
Shridhar Gupta,

1
Bella soluzione, facile da seguire. Non ho racchiuso il mio in una funzione, quindi per risolvere alla fine invece di mettere return sequence;ho messosequence.then(() => { do stuff });
Joe Coyle

25

Utilità semplice per la promessa standard di Node.js:

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

AGGIORNARE

items-promise è un pacchetto NPM pronto all'uso che fa lo stesso.


6
Mi piacerebbe vederlo spiegato in maggior dettaglio.
Tyguy7,

Ho fornito una variante di questa risposta con la spiegazione di seguito. Grazie
Sarsaparilla il

Questo è esattamente ciò che faccio in ambienti pre-Nodo 7 che non hanno accesso a Async / waitit. Bello e pulito.
JHH,

11

Ho dovuto eseguire molte attività sequenziali e ho usato queste risposte per creare una funzione che si occupasse di gestire qualsiasi attività sequenziale ...

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

La funzione accetta 2 argomenti + 1 opzionale. Il primo argomento è l'array su cui lavoreremo. Il secondo argomento è l'attività stessa, una funzione che restituisce una promessa, l'attività successiva verrà avviata solo quando questa promessa si risolve. Il terzo argomento è un callback da eseguire quando tutte le attività sono state eseguite. Se non viene passato alcun callback, la funzione restituisce la promessa creata in modo da poter gestire la fine.

Ecco un esempio di utilizzo:

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

Spero che salvi qualcuno un po 'di tempo ...


Incredibile soluzione, è stata la migliore che ho trovato in quasi una settimana di contrabbando .... È molto ben spiegato, ha nomi interiori logici, un buon esempio (potrebbe essere migliore), posso chiamarlo in sicurezza come tanti volte in base alle necessità e include l'opzione per impostare i callback. semplicemente bello! (Ho appena cambiato il nome in qualcosa che mi ha più senso) .... RACCOMANDAZIONE per gli altri ... puoi iterare un oggetto usando 'Object.keys ( myObject )' come tuo 'object_array'
DavidTaubmann

Grazie per il tuo commento! Non sto nemmeno usando quel nome, ma volevo renderlo più ovvio / semplice qui.
Salketer,

5

La soluzione più bella che sono riuscito a capire è stata con le bluebirdpromesse. Puoi semplicemente fare Promise.resolve(files).each(fs.readFileAsync);quali garanzie che le promesse vengano risolte in ordine sequenziale.


1
Ancora meglio: Promise.each(filtes, fs.readFileAsync). A proposito, non devi farlo .bind(fs)?
Bergi,

Nessuno qui sembra capire la differenza tra una matrice e una sequenza, che quest'ultima implica dimensioni illimitate / dinamiche.
vitaly-t

Si noti che le matrici in Javascript non hanno nulla a che fare con le matrici di dimensioni fisse nei linguaggi in stile C. Sono solo oggetti con gestione numerica della chiave fissata e non hanno dimensioni o limiti prescritti ( soprattutto non durante l'uso new Array(int). Tutto ciò che fa è preimpostare la lengthcoppia chiave-valore, influenzando il numero di indici utilizzati durante l'iterazione basata sulla lunghezza. Ha zero effetto sull'indicizzazione o sui limiti dell'indice effettivi dell'array)
Mike 'Pomax' Kamermans,

4

Questa è una leggera variazione di un'altra risposta sopra. Utilizzando promesse native:

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

Spiegazione

Se hai questi compiti [t1, t2, t3], allora quanto sopra è equivalente a Promise.resolve().then(t1).then(t2).then(t3). È il comportamento di ridurre.

Come usare

Per prima cosa devi costruire un elenco di attività! Un'attività è una funzione che non accetta alcun argomento. Se è necessario passare argomenti alla funzione, utilizzare bindo altri metodi per creare un'attività. Per esempio:

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

4

La mia soluzione preferita:

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

Non è sostanzialmente diverso dagli altri pubblicati qui ma:

  • Applica la funzione agli articoli in serie
  • Risolve una serie di risultati
  • Non richiede asincronizzazione / attesa (il supporto è ancora abbastanza limitato, circa 2017)
  • Utilizza le funzioni freccia; simpatica e concisa

Esempio di utilizzo:

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

Testato su Chrome (v59) e NodeJS correnti ragionevoli (v8.1.2).


3

Usa Array.prototype.reducee ricorda di racchiudere le tue promesse in una funzione, altrimenti saranno già in esecuzione!

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

bello e facile ... dovresti essere in grado di riutilizzare lo stesso seme per le prestazioni, ecc.

È importante proteggersi da array vuoti o array con solo 1 elemento quando si utilizza il comando di riduzione , quindi questa tecnica è la soluzione migliore:

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

e poi chiamalo come:

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

2

Ho creato questo semplice metodo sull'oggetto Promise:

Creare e aggiungere un metodo Promise.sequence all'oggetto Promise

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

Uso:

var todo = [];

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

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

La cosa migliore di questa estensione per l'oggetto Promise è che è coerente con lo stile delle promesse. Promise.all e Promise.sequence sono invocati allo stesso modo, ma hanno una semantica diversa.

Attenzione

La gestione sequenziale delle promesse di solito non è un ottimo modo per utilizzare le promesse. Di solito è meglio usare Promise.all e lasciare che il browser esegua il codice il più velocemente possibile. Tuttavia, ci sono casi d'uso reali per questo, ad esempio quando si scrive un'app mobile usando JavaScript.


No, non puoi confrontare Promise.alle il tuo Promise.sequence. Uno prende un iterabile di promesse, l'altro prende una serie di funzioni che restituiscono promesse.
Bergi,

A proposito, consiglierei di evitare l' antipasto costruttore di promesse
Bergi,

Non sapevo che ci volesse un iteratore. Dovrebbe essere abbastanza facile da riscriverlo però. Potresti spiegare perché questo è il promettente antipattern del costruttore? Ho fatto leggere il tuo post qui: stackoverflow.com/a/25569299/1667011
frodeborli

@Bergi Ho aggiornato il codice per supportare gli iteratori. Ancora non vedo che questo è un antipasto. Gli antipattern in genere devono essere considerati linee guida per evitare errori di codifica ed è perfettamente valido creare funzioni (librerie) che infrangono tali linee guida.
frodeborli,

Sì, se la consideri una funzione di libreria è OK, ma comunque in questo caso reducela risposta di Benjamin è molto più semplice.
Bergi,

2

È possibile utilizzare questa funzione che ottiene la lista promesse:

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

Promise Factory è solo una semplice funzione che restituisce una Promessa:

function myPromiseFactory() {
    return somethingThatCreatesAPromise();
}

Funziona perché una fabbrica di promesse non crea la promessa fino a quando non viene richiesto. Funziona allo stesso modo di una funzione allora - in effetti, è la stessa cosa!

Non vuoi affatto operare su una serie di promesse. Secondo le specifiche Promise, non appena viene creata una promessa, inizia l'esecuzione. Quindi quello che vuoi davvero è una serie di fabbriche promettenti ...

Se vuoi saperne di più su Promises, dovresti controllare questo link: https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html


2

La mia risposta si basa su 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;
    });
  };

Questa soluzione restituisce i risultati come un array come Promise.all ().

Uso:

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

2

Mi è piaciuta molto la risposta di @ joelnet, ma per me quello stile di codifica è un po 'difficile da digerire, quindi ho trascorso un paio di giorni cercando di capire come avrei espresso la stessa soluzione in un modo più leggibile e questa è la mia prendere, solo con una sintassi diversa e alcuni commenti.

// 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

Come ha notato Bergi, penso che la soluzione migliore e chiara sia usare BlueBird.each, codice qui sotto:

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

2

Innanzitutto, devi capire che una promessa viene eseguita al momento della creazione.
Ad esempio, se hai un codice:

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

Devi cambiarlo in:

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

Quindi dobbiamo concatenare sequenzialmente le promesse:

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

l'esecuzione after(), farà in modo che la promessa venga creata (ed eseguita) solo quando sarà il momento.


1

Uso il seguente codice per estendere l'oggetto Promise. Gestisce il rifiuto delle promesse e restituisce una serie di risultati

Codice

/*
    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);
        });
    });
};

Esempio

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

Se lo desideri, puoi utilizzare la riduzione per fare una promessa sequenziale, ad esempio:

[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));

funzionerà sempre in sequenza.


1

Utilizzando ES moderno:

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

Con Async / Await (se hai il supporto di ES7)

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

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

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

(devi usare il forciclo, e non forEachperché async / await ha problemi nell'esecuzione in ogni ciclo)

Senza Asincrono / Attendi (usando 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
Attendere dentro per Ogni non è raccomandato.
Marcelo Agimóvel,

@ MarceloAgimóvel - Ho aggiornato alla soluzione per non funzionare forEach(secondo questo )
Gil Epshtain

0

Sulla base del titolo della domanda "Risolvi le promesse una dopo l'altra (cioè in sequenza)?", Potremmo capire che l'OP è più interessato alla gestione sequenziale delle promesse di liquidazione rispetto alle chiamate sequenziali di per sé .

Questa risposta è offerta:

  • per dimostrare che le chiamate sequenziali non sono necessarie per la gestione sequenziale delle risposte.
  • esporre modelli alternativi praticabili ai visitatori di questa pagina, incluso l'OP se è ancora interessato più di un anno dopo.
  • nonostante l'affermazione del PO di non voler effettuare chiamate contemporaneamente, il che può essere davvero il caso, ma allo stesso modo può essere un presupposto basato sul desiderio di una gestione sequenziale delle risposte come suggerisce il titolo.

Se le chiamate simultanee non sono veramente richieste, vedere la risposta di Benjamin Gruenbaum che copre in modo completo le chiamate sequenziali (ecc.).

Se tuttavia sei interessato (per migliorare le prestazioni) a schemi che consentono chiamate simultanee seguite da una gestione sequenziale delle risposte, continua a leggere.

È allettante pensare di dover usare Promise.all(arr.map(fn)).then(fn)(come ho fatto molte volte) o lo zucchero di fantasia di una Promise lib (in particolare di Bluebird), tuttavia (con il merito di questo articolo ) uno arr.map(fn).reduce(fn)schema farà il lavoro, con i vantaggi che:

  • funziona con qualsiasi lib promessa - anche versioni pre-conformi di jQuery - solo .then() viene utilizzato .
  • offre la flessibilità di saltare l'errore o lo stop-on-error, a seconda di quale si desideri con un mod a una riga.

Eccolo, scritto per 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.
    });
};

Nota: solo quel frammento, Q() è specifico di Q. Per jQuery è necessario assicurarsi che readFile () restituisca una promessa jQuery. Con A + libs, le promesse straniere saranno assimilate.

La chiave qui è la sequencepromessa della riduzione , che mette in sequenza la gestione delle readFilepromesse ma non la loro creazione.

E una volta assorbito, forse è un po 'strabiliante quando ti rendi conto che il .map()palco non è effettivamente necessario! L'intero lavoro, le chiamate parallele e la gestione seriale nell'ordine corretto, possono essere raggiunti da reduce()soli, oltre al vantaggio aggiuntivo di un'ulteriore flessibilità per:

  • convertire da chiamate asincrone parallele a chiamate asincrone seriali semplicemente spostando una linea - potenzialmente utile durante lo sviluppo.

Eccolo, di Qnuovo.

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

Questo è lo schema di base. Se si volesse anche fornire dati al chiamante (ad es. I file o una loro trasformazione), sarebbe necessaria una variante leggera.


Non credo sia una buona idea rispondere a domande contrarie alle intenzioni dei PO ...
Bergi,

1
Questa sequence.then(() => filePromise)cosa è un antipattern - non propaga errori non appena potrebbero (e crea unhandledRejectionin lib che li supportano). Preferisci usare Q.all([sequence, filePromise])o $.when(sequence, filePromise). Certo, questo comportamento potrebbe essere quello che vuoi quando miri di ignorare o saltare gli errori, ma dovresti almeno menzionarlo come uno svantaggio.
Bergi,

@Bergi, spero che l'OP interverrà e fornirà un giudizio sul fatto che questo sia veramente contrario alle sue intenzioni o meno. In caso contrario, eliminerò la risposta immagino, nel frattempo spero di aver giustificato la mia posizione. Grazie per averlo preso abbastanza sul serio da fornire un feedback decente. Puoi spiegare di più sull'anti-pattern o fornire un riferimento per favore? Lo stesso vale per l'articolo in cui ho trovato il modello di base ?
Roamer-1888,

1
Sì, la terza versione del suo codice (ovvero "sia parallela che sequenziale") presenta lo stesso problema. L '"antipattern" necessita di una sofisticata gestione degli errori ed è incline a collegare i gestori in modo asincrono, causando unhandledRejectioneventi. In Bluebird puoi aggirare il problema usando sequence.return(filePromise)quale ha lo stesso comportamento ma gestisce bene i rifiuti. Non conosco alcun riferimento, l'ho appena trovato - non credo che il "(anti) modello" abbia ancora un nome.
Bergi,

1
@Bergi, puoi vedere chiaramente qualcosa che non posso :( Mi chiedo se questo nuovo anti-pattern debba essere documentato da qualche parte?
Roamer-1888,

0

Il tuo approccio non è male, ma ha due problemi: ingoia errori e utilizza l'Explicit Promise Construction Antipattern.

Puoi risolvere entrambi questi problemi e rendere il codice più pulito, pur utilizzando la stessa strategia generale:

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

Se qualcun altro ha bisogno di un modo garantito per STRETTAMENTE il modo sequenziale di risolvere le Promesse quando si eseguono operazioni CRUD, è anche possibile utilizzare il seguente codice come base.

Fintanto che aggiungi 'return' prima di chiamare ciascuna funzione, descrivendo una Promessa, e usi questo esempio come base, la successiva chiamata alla funzione .then () inizierà IN CONTINUO dopo il completamento di quella precedente:

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

Il metodo push e pop array può essere utilizzato per la sequenza delle promesse. Puoi anche spingere nuove promesse quando hai bisogno di dati aggiuntivi. Questo è il codice che userò nel caricatore React Infinite per caricare la sequenza di pagine.

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

La maggior parte delle risposte non include i risultati di TUTTE le promesse singolarmente, quindi nel caso in cui qualcuno stia cercando questo comportamento particolare, questa è una possibile soluzione usando la ricorsione.

Segue lo stile di Promise.all:

  • Restituisce l'array di risultati nel .then()callback.

  • Se una promessa fallisce, viene restituita immediatamente nel .catch()callback.

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

Nota sulla tasksdichiarazione dell'array :

In questo caso non è possibile utilizzare la seguente notazione come si Promise.allfarebbe:

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

E dobbiamo usare:

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

Il motivo è che JavaScript inizia a eseguire la promessa immediatamente dopo la sua dichiarazione. Se usiamo metodi simili Promise.all, verifica semplicemente che lo stato di tutti è fulfilledo rejected, ma non avvia l'exezione stessa. Usando () => promise()fermiamo l'esecuzione fino a quando non viene chiamato.


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

Qui il tasto è come si chiama la funzione sleep. È necessario passare una serie di funzioni che a sua volta restituisce una promessa invece di una serie di promesse.


-1

Questo per estendere il modo in cui elaborare una sequenza di promesse in modo più generico, supportando sequenze dinamiche / infinite, basate sull'implementazione di spex.sequence :

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

Non solo questa soluzione funzionerà con sequenze di qualsiasi dimensione, ma è possibile aggiungere facilmente la limitazione dei dati e il bilanciamento del carico .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.