Come sincronizzare una sequenza di promesse?

Ho una serie di oggetti di promise che devono essere risolti nella stessa sequenza in cui sono elencati nell’array, ovvero non possiamo tentare di risolvere un elemento fino a quando il precedente non è stato risolto (come metodo Promise.all([...]) fa).

E se un elemento viene rifiutato, ho bisogno che la catena rigetti immediatamente, senza tentare di risolvere il seguente elemento.

Come posso implementarlo o esiste un’implementazione esistente per questo modello di sequence ?

 function sequence(arr) { return new Promise(function (resolve, reject) { // try resolving all elements in 'arr', // but strictly one after another; }); } 

MODIFICARE

Le risposte iniziali suggeriscono che possiamo solo sequence risultati di tali elementi di matrice, non la loro esecuzione, perché è predefinito in tale esempio.

Ma come generare una serie di promesse in modo tale da evitare l’esecuzione anticipata?

Ecco un esempio modificato:

 function sequence(nextPromise) { // while nextPromise() creates and returns another promise, // continue resolving it; } 

Non vorrei farlo in una domanda separata, perché credo che faccia parte dello stesso problema.

SOLUZIONE

Alcune risposte di seguito e le discussioni che seguirono andarono un po ‘fuori strada, ma l’eventuale soluzione che fece esattamente ciò che stavo cercando fu implementata all’interno della libreria spex , come sequenza di metodi. Il metodo può scorrere attraverso una sequenza di lunghezza dynamic e creare promesse come richiesto dalla logica aziendale dell’applicazione.

Più tardi l’ho trasformato in una libreria condivisa che tutti possono usare.

Ecco alcuni semplici esempi su come eseguire una sequenza attraverso una matrice eseguendo ogni operazione asincrona in serie (una dopo l’altra).

Supponiamo che tu abbia una serie di elementi:

 var arr = [...]; 

E, si desidera eseguire una specifica operazione asincrona su ciascun elemento nell’array, una alla volta in modo seriale in modo tale che l’operazione successiva non inizi finché non è terminata quella precedente.

E, supponiamo che tu abbia una funzione di ritorno promise per l’elaborazione di uno degli elementi dell’array:

Iterazione manuale

 function processItem(item) { // do async operation and process the result // return a promise } 

Quindi, puoi fare qualcosa del genere:

 function processArray(array, fn) { var index = 0; function next() { if (index < array.length) { fn(array[index++]).then(next); } } next(); } processArray(arr, processItem); 

Promessa di ritorno dell'iterazione manuale

Se si desidera una promise restituita da processArray() modo da sapere quando è stata completata, è ansible aggiungerla a questo:

 function processArray(array, fn) { var index = 0; return new Promise(function(resolve, reject) { function next() { if (index < array.length) { fn(array[index++]).then(next, reject); } else { resolve(); } } next(); } } processArray(arr, processItem).then(function() { // all done here }, function(reason) { // rejection happened }); 

Nota: ciò interromperà la catena al primo rifiuto e riporterà tale motivo al processArray restituito.

Iterazione con .reduce ()

Se volessi fare più del lavoro con le promesse, potresti incatenare tutte le promesse:

 function processArray(array, fn) { return array.reduce(function(p, item) { return p.then(function() { return fn(item); }); }, Promise.resolve()); } processArray(arr, processItem).then(function(result) { // all done here }, function(reason) { // rejection happened }); 

Nota: ciò interromperà la catena al primo rifiuto e passerà tale motivo alla promise restituita da processArray() .

Per uno scenario di successo, la promise restituita da processArray() verrà risolta con l'ultimo valore risolto del callback fn . Se si desidera accumulare un elenco di risultati e risolverlo, è ansible raccogliere i risultati in un array di chiusura da fn e continuare a restituire tale array ogni volta in modo che la risoluzione finale sia una serie di risultati.

Iterazione con .reduce () che risolve con l'array

E, poiché ora sembra evidente che vuoi che il risultato finale promesso sia una matrice di dati (in ordine), ecco una revisione della soluzione precedente che produce questo:

 function processArray(array, fn) { var results = []; return array.reduce(function(p, item) { return p.then(function() { return fn(item).then(function(data) { results.push(data); return results; }); }); }, Promise.resolve()); } processArray(arr, processItem).then(function(result) { // all done here // array of data here in result }, function(reason) { // rejection happened }); 

Demo funzionante: http://jsfiddle.net/jfriend00/h3zaw8u8/

E una demo funzionante che mostra un rifiuto: http://jsfiddle.net/jfriend00/p0ffbpoc/

Iterazione con .reduce () che risolve con array con ritardo

E, se vuoi inserire un piccolo ritardo tra le operazioni:

 function delay(t, v) { return new Promise(function(resolve) { setTimeout(resolve.bind(null, v), t); }); } function processArrayWithDelay(array, t, fn) { var results = []; return array.reduce(function(p, item) { return p.then(function() { return fn(item).then(function(data) { results.push(data); return delay(t, results); }); }); }, Promise.resolve()); } processArray(arr, 200, processItem).then(function(result) { // all done here // array of data here in result }, function(reason) { // rejection happened }); 

Iterazione con Bluebird Promise Library

La libreria di promise Bluebird ha un sacco di funzioni di controllo della concorrenza integrate. Ad esempio, per sequenziare l'iterazione attraverso un array, è ansible utilizzare Promise.mapSeries() .

 Promise.mapSeries(arr, function(item) { // process each individual item here, return a promise return processItem(item); }).then(function(results) { // process final results here }).catch(function(err) { // process array here }); 

O per inserire un ritardo tra le iterazioni:

 Promise.mapSeries(arr, function(item) { // process each individual item here, return a promise return processItem(item).delay(100); }).then(function(results) { // process final results here }).catch(function(err) { // process array here }); 

Usando ES7 async / await

Se stai codificando in un ambiente che supporta async / await, puoi anche semplicemente usare un ciclo for regolare e quindi await una promise nel ciclo e farà sì che il ciclo for interrompa fino a quando una promise non viene risolta prima di procedere. Questo eseguirà in modo efficace le operazioni asincrone in modo che il successivo non inizi finché non viene eseguito quello precedente.

 async function processArray(array, fn) { let results = []; for (let i = 0; i < array.length; i++) { let r = await fn(array[i]); results.push(r); } return results; // will be resolved value of promise } // sample usage processArray(arr, processItem).then(function(result) { // all done here // array of data here in result }, function(reason) { // rejection happened }); 

Per processArray() informazione, penso che la mia funzione processArray() qui sia molto simile a Promise.map() nella libreria di promise Bluebird che accetta un array e una funzione che produce una promise e restituisce una promise che si risolve con una serie di risultati risolti.


@ vitaly-t - Ecco alcuni commenti più dettagliati sul tuo approccio. Sei il benvenuto a qualsiasi codice ti sembra meglio. Quando ho iniziato a usare le promesse, tendevo a usare le promesse solo per le cose più semplici che facevano e scrivevo molto della logica quando un uso più avanzato delle promesse poteva farne molto di più per me. Usi solo ciò di cui sei pienamente a tuo agio e al di là di questo, preferiresti vedere il tuo codice che conosci intimamente. Probabilmente è la natura umana.

Ti suggerirò che mentre capisco sempre più di ciò che le promesse possono fare per me, ora mi piace scrivere un codice che usi più delle funzioni avanzate delle promesse e mi sembra perfettamente naturale e mi sento come se stessimo bene infrastruttura testata che ha molte funzioni utili. Ti chiederei solo di tenere la mente aperta mentre impari sempre di più ad andare in quella direzione. È mia opinione che sia una direzione utile e produttiva migrare man mano che la tua comprensione migliora.

Ecco alcuni punti specifici di feedback sul tuo approccio:

Crei le promesse in sette posti

In contrasto con gli stili, il mio codice ha solo due punti in cui creo esplicitamente una nuova promise: una volta nella funzione factory e una volta per inizializzare il ciclo .reduce() . Ovunque, sto solo costruendo le promesse già create concatenando loro o restituendo valori al loro interno o semplicemente restituendole direttamente. Il tuo codice ha sette luoghi unici in cui stai creando una promise. Ora, una buona codifica non è un concorso per vedere in quali pochi posti è ansible creare una promise, ma ciò potrebbe indicare la differenza nel far leva sulle promesse che sono già state create rispetto alle condizioni di test e sulla creazione di nuove promesse.

La sicurezza del tiro è una caratteristica molto utile

Le promesse sono sicure. Ciò significa che un'eccezione generata all'interno di un gestore di promesse rigetterà automaticamente quella promise. Se si desidera che l'eccezione diventi un rifiuto, questa è una funzionalità molto utile da utilizzare. In effetti, scoprirai che il solo buttare te stesso è un modo utile per rifiutare da un conduttore senza creare un'altra promise.

Un sacco di Promise.resolve() o Promise.reject() è probabilmente un'opportunità di semplificazione

Se vedi codice con molte Promise.resolve() o Promise.reject() , allora ci sono probabilmente opportunità di sfruttare meglio le promesse esistenti piuttosto che creare tutte queste nuove promesse.

Trasmetti a una promise

Se non sai se qualcosa ha restituito una promise, allora puoi lanciarla su una promise. La libreria di promise quindi eseguirà i propri controlli sia che si tratti di una promise o meno e anche se è il tipo di promise che corrisponde alla libreria di promise che si sta utilizzando e, in caso contrario, racchiuderla in una sola. Questo può salvare da solo la riscrittura di questa logica.

Contratto per restituire una promise

In molti casi al giorno d'oggi, è completamente fattibile avere un contratto per una funzione che possa fare qualcosa di asincrono per restituire una promise. Se la funzione vuole solo fare qualcosa di sincrono, allora può solo restituire una promise risolta. Sembra che tu sia oneroso, ma è sicuramente il modo in cui soffia il vento e già scrivo un sacco di codice che lo richiede e sembra molto naturale una volta acquisita familiarità con le promesse. Elimina se l'operazione è sincronizzata o asincrona e il chiamante non deve sapere o fare qualcosa di speciale in entrambi i casi. Questo è un buon uso delle promesse.

La funzione di fabbrica può essere scritta per creare una sola promise

La funzione di fabbrica può essere scritta per creare una sola promise e quindi risolverla o rifiutarla. Questo stile lo rende anche sicuro, quindi ogni eccezione che si verifica nella funzione di fabbrica diventa automaticamente un rifiuto. Rende anche il contratto per restituire sempre una promise automatica.

Mentre mi rendo conto che questa funzione di fabbrica è una funzione segnaposto (non fa nulla di asincrono), si spera che tu possa vedere lo stile per considerarlo:

 function factory(idx) { // create the promise this way gives you automatic throw-safety return new Promise(function(resolve, reject) { switch (idx) { case 0: resolve("one"); break; case 1: resolve("two"); break; case 2: resolve("three"); break; default: resolve(null); break; } }); } 

Se una qualsiasi di queste operazioni fosse asincrona, allora potrebbero semplicemente restituire le proprie promesse che automaticamente si incatenerebbero all'unica promise centrale come questa:

 function factory(idx) { // create the promise this way gives you automatic throw-safety return new Promise(function(resolve, reject) { switch (idx) { case 0: resolve($.ajax(...)); case 1: resole($.ajax(...)); case 2: resolve("two"); break; default: resolve(null); break; } }); } 

Utilizzare un gestore di rifiuti per return promise.reject(reason) semplicemente return promise.reject(reason) non è necessario

Quando hai questo codice di codice:

  return obj.then(function (data) { result.push(data); return loop(++idx, result); }, function (reason) { return promise.reject(reason); }); 

Il gestore di rifiuti non aggiunge alcun valore. Puoi invece farlo semplicemente:

  return obj.then(function (data) { result.push(data); return loop(++idx, result); }); 

Stai già restituendo il risultato di obj.then() . Se obj rifiuta o se qualcosa incatenato a obj o restituito da quel .then() gestore respinge, quindi obj rifiuterà. Quindi non è necessario creare una nuova promise con il rifiuto. Il codice più semplice senza il gestore rifiuta fa la stessa cosa con meno codice.


Ecco una versione dell'architettura generale del tuo codice che tenta di incorporare la maggior parte di queste idee:

 function factory(idx) { // create the promise this way gives you automatic throw-safety return new Promise(function(resolve, reject) { switch (idx) { case 0: resolve("zero"); break; case 1: resolve("one"); break; case 2: resolve("two"); break; default: // stop further processing resolve(null); break; } }); } // Sequentially resolves dynamic promises returned by a factory; function sequence(factory) { function loop(idx, result) { return Promise.resolve(factory(idx)).then(function(val) { // if resolved value is not null, then store result and keep going if (val !== null) { result.push(val); // return promise from next call to loop() which will automatically chain return loop(++idx, result); } else { // if we got null, then we're done so return results return result; } }); } return loop(0, []); } sequence(factory).then(function(results) { log("results: ", results); }, function(reason) { log("rejected: ", reason); }); 

Demo funzionante: http://jsfiddle.net/jfriend00/h3zaw8u8/

Alcuni commenti su questa implementazione:

  1. Promise.resolve(factory(idx)) essenzialmente il risultato di factory(idx) in una promise. Se era solo un valore, allora diventa una promise risolta con quel valore di ritorno come valore di risoluzione. Se era già una promise, allora si incatena a quella promise. Quindi, sostituisce tutto il codice di controllo del tipo sul valore restituito dalla funzione factory() .

  2. La funzione factory segnala che viene eseguita restituendo null o una promise il cui valore risolto è null . Il cast precedente associa queste due condizioni allo stesso codice risultante.

  3. La funzione factory rileva automaticamente le eccezioni e le trasforma in scarti che vengono quindi gestiti automaticamente dalla funzione sequence() . Questo è un vantaggio significativo di lasciare che le promesse facciano molta della gestione degli errori se si desidera interrompere l'elaborazione e riportare l'errore alla prima eccezione o rifiuto.

  4. La funzione di fabbrica in questa implementazione può restituire una promise o un valore statico (per un'operazione sincrona) e funzionerà correttamente (secondo la richiesta di progettazione).

  5. L'ho provato con un'eccezione generata nella callback di promise nella funzione factory e in effetti rifiuta e ritriggers quella eccezione per rifiutare la promise della sequenza con l'eccezione come motivo.

  6. Questo metodo usa un metodo simile a te (di proposito, cercando di stare con la tua architettura generale) per concatenare più chiamate a loop() .

Le promesse rappresentano i valori delle operazioni e non le operazioni stesse. Le operazioni sono già iniziate, quindi non puoi farle aspettare l’una per l’altra.

Invece, è ansible sincronizzare le funzioni che restituiscono promesse richiamandole nell’ordine (tramite un ciclo con concatenazione promettente per esempio), o usando il metodo .each in bluebird.

Non puoi semplicemente eseguire X operazioni asincrone e poi vuoi che vengano risolte in un ordine.

Il modo corretto di fare qualcosa di simile è di eseguire la nuova operazione asincrona solo dopo che è stata risolta una prima:

 doSomethingAsync().then(function(){ doSomethingAsync2().then(function(){ doSomethingAsync3(); ....... }); }); 

modificare
Sembra che tu voglia aspettare tutte le promesse e quindi richiamare i loro callback in un ordine specifico. Qualcosa come questo:

 var callbackArr = []; var promiseArr = []; promiseArr.push(doSomethingAsync()); callbackArr.push(doSomethingAsyncCallback); promiseArr.push(doSomethingAsync1()); callbackArr.push(doSomethingAsync1Callback); ......... promiseArr.push(doSomethingAsyncN()); callbackArr.push(doSomethingAsyncNCallback); 

e poi:

 $.when(promiseArr).done(function(promise){ while(callbackArr.length > 0) { callbackArr.pop()(promise); } }); 

I problemi che possono verificarsi con questo sono quando una o più promesse falliscono.

Anche se piuttosto denso, ecco un’altra soluzione che eseguirà una funzione promettente di restituzione su una serie di valori e risolverà con una serie di risultati:

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

Uso:

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

Tieni presente che, poiché stiamo riducendo da una promise alla successiva, ogni elemento viene elaborato in serie.

È funzionalmente equivalente alla soluzione “Iterazione con. Riduzione () che risolve con l’array” di @ jfriend00 ma un po ‘più ordinata.

Suppongo due approcci per la gestione di questa domanda:

  1. Crea più promesse e usa la funzione allWithAsync come segue:
 let allPromiseAsync = (...PromisesList) => { return new Promise(async resolve => { let output = [] for (let promise of PromisesList) { output.push(await promise.then(async resolvedData => await resolvedData)) if (output.length === PromisesList.length) resolve(output) } }) } const prm1= Promise.resolve('first'); const prm2= new Promise((resolve, reject) => setTimeout(resolve, 2000, 'second')); const prm3= Promise.resolve('third'); allPromiseAsync(prm1, prm2, prm3) .then(resolvedData => { console.log(resolvedData) // ['first', 'second', 'third'] }); 
  1. Utilizza invece la funzione Promise.all:
  (async () => { const promise1 = new Promise(resolve => { setTimeout(() => { resolve() }, 2500) }) const promise2 = new Promise(resolve => { setTimeout(() => { resolve() }, 5000) }) const promise3 = new Promise(resolve => { setTimeout(() => { resolve() }, 1000) }) const promises = [promise1, promise2, promise3] await Promise.all(promises) console.log('This line is shown after 8500ms') })()