¿Resolver promesas una tras otra (es decir, en secuencia)?


269

Considere el siguiente código que lee una matriz de archivos de manera serial / secuencial. readFilesdevuelve una promesa, que se resuelve solo una vez que todos los archivos se han leído en secuencia.

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!

  });
};

El código anterior funciona, pero no me gusta tener que recurrir para que las cosas ocurran secuencialmente. ¿Hay alguna manera más simple de que este código se pueda volver a escribir para que no tenga que usar mi extrañoreadSequential función?

Originalmente intenté usarlo Promise.all, pero eso provocó que todas las readFilellamadas tuvieran lugar simultáneamente, lo cual no es lo que quiero:

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

2
Cualquier cosa que tenga que esperar a que finalice una operación asincrónica anterior debe hacerse en una devolución de llamada. Usar promesas no cambia eso. Entonces necesitas la recursión.
Barmar

1
Para su información, esto no es técnicamente una recursión ya que no hay acumulación de marcos de pila. Lo anterior readFileSequential()ya ha regresado antes de que se llame al siguiente (porque es asíncrono, se completa mucho después de que la llamada a la función original ya haya regresado).
jfriend00

1
@ jfriend00 La acumulación de cuadros de pila no es necesaria para la recursión, solo para autorreferencia. Sin embargo, esto es solo un tecnicismo.
Benjamin Gruenbaum

3
@BenjaminGruenbaum: mi punto es que no hay absolutamente nada de malo en que la función se llame a sí misma para iniciar la próxima iteración. No tiene ningún inconveniente y, de hecho, es una forma eficiente de secuenciar operaciones asíncronas. Por lo tanto, no hay razón para evitar algo que parece recurrencia. Hay soluciones recursivas para algunos problemas que son ineficientes, este no es uno de esos.
jfriend00

1
Hola, por una discusión y solicitud en la sala de JavaScript, he editado esta respuesta para que podamos señalar a otros como canónica. Si no está de acuerdo, hágamelo saber y lo restauraré y abriré uno por separado.
Benjamin Gruenbaum

Respuestas:


338

Actualización 2017 : usaría una función asíncrona si el entorno lo admite:

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

Si lo desea, puede aplazar la lectura de los archivos hasta que los necesite utilizando un generador asíncrono (si su entorno lo admite):

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

Actualización: Pensándolo bien, podría usar un bucle for en su lugar:

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

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

O más compacto, con reducción:

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

En otras bibliotecas prometedoras (como when y Bluebird) tiene métodos de utilidad para esto.

Por ejemplo, Bluebird sería:

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

Aunque realmente no hay razón para no usar async, aguarde hoy.


2
@ EmreTapcı, no. Una función de flecha "=>" ya implica regresar.
Max

Si usa TypeScript, creo que la solución de bucle "for in" es la mejor. Reducir los retornos Promesas recursivas, por ejemplo. el primer tipo de devolución de llamada es Promise <void>, luego el segundo es Promise <Promise <void>> y así sucesivamente - es imposible escribir sin usar ninguno, creo
Artur Tagisow

@ArturTagisow TypeScript (al menos las nuevas versiones) tienen tipos recursivos y deberían resolver los tipos correctamente aquí. No existe una Promesa <Promesa <T>> ya que las promesas "se asimilan recursivamente". Promise.resolve(Promise.resolve(15))es idéntico al Promise.resolve(15).
Benjamin Gruenbaum


72

Así es como prefiero ejecutar tareas en 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 ----");
        });
}

¿Qué pasa con los casos con más tareas? Como, 10?

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

8
¿Y qué hay de los casos en los que no conoce el número exacto de tareas?
maldito

1
¿Y qué pasa cuando sabes la cantidad de tareas, pero solo en tiempo de ejecución?
joeytwiddle

10
"no desea operar sobre una serie de promesas. Según la especificación de la promesa, tan pronto como se crea una promesa, comienza a ejecutarse. Entonces, lo que realmente quiere es una serie de fábricas de promesas", vea Error avanzado n.º 3 aquí: pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
edelans

55
Si le gusta reducir el ruido de la línea, también puede escribirresult = result.then(task);
Daniel Buckmaster, el

1
@DanielBuckmaster sí, pero tenga cuidado, ya que si task () devuelve un valor, se pasará a la próxima invocación. Si su tarea tiene argumentos opcionales, esto podría causar efectos secundarios. El código actual se traga los resultados e invoca explícitamente la siguiente tarea sin argumentos.
JHH

63

Esta pregunta es antigua, pero vivimos en un mundo de ES6 y JavaScript funcional, así que veamos cómo podemos mejorar.

Debido a que las promesas se ejecutan de inmediato, no podemos simplemente crear una serie de promesas, todas se dispararían en paralelo.

En cambio, necesitamos crear una serie de funciones que devuelva una promesa. Cada función se ejecutará secuencialmente, lo que iniciará la promesa en su interior.

Podemos resolver esto de varias maneras, pero mi forma favorita es usar reduce.

Se vuelve un poco complicado usarlo reduceen combinación con las promesas, por lo que he dividido el revestimiento en algunas picaduras más pequeñas que se pueden digerir a continuación.

La esencia de esta función es usarla reducecomenzando con un valor inicial de Promise.resolve([]), o una promesa que contenga una matriz vacía.

Esta promesa se pasará al reducemétodo como promise. Esta es la clave para encadenar cada promesa en forma secuencial. La próxima promesa de ejecución es funcy cuando se thendispara, los resultados se concatenan y esa promesa se devuelve, ejecutando el reduceciclo con la siguiente función de promesa.

Una vez que se hayan ejecutado todas las promesas, la promesa devuelta contendrá una serie de todos los resultados de cada promesa.

Ejemplo ES6 (un revestimiento)

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

Ejemplo ES6 (desglosado)

// 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
muy bueno, gracias, Array.prototype.concat.bind(result)es la parte que me faltaba, tenía que empujar a los resultados manualmente que funcionó pero fue menos genial
zavr

Dado que todos somos sobre JS moderno, creo que la console.log.bind(console)declaración en su último ejemplo ahora es generalmente innecesaria. En estos días puedes pasar console.log. P.ej. serial(funcs).then(console.log). Probado en nodejs actuales y Chrome.
Molomby

Esto fue un poco difícil de entender, pero la reducción está haciendo esto correctamente. 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í, esto parece correcto. También puede soltar Promise.resolve en la devolución, cualquier valor devuelto se resolverá automáticamente a menos que llame a Promise.reject en ellos.
joelnet

@joelnet, en respuesta al comentario de danecando, creo que lo que debería hacer la reducción debería ser más correcto en la siguiente expresión, ¿estás de acuerdo? Promise.resolve([]).then(x => someApiCall('url1').then(r => x.concat(r))).then(x => someApiCall('url2').then(r => x.concat(r)))y así sucesivamente
bufferoverflow76

37

Para hacer esto simplemente en 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
Parece que está usando guión bajo. Puede simplificar files.forEachsi los archivos son una matriz.
Gustavo Rodrigues

2
Bueno ... es ES5. La forma ES6 sería for (file of files) {...}.
Gustavo Rodrigues

1
Dices que no deberías usar Promise.resolve()para crear una promesa ya resuelta en la vida real. Por qué no? Promise.resolve()parece más limpio que new Promise(success => success()).
canac

8
@canac Lo siento, era solo una broma con un juego de palabras ("promesas vacías ..."). Definitivamente use Promise.resolve();en su código.
Shridhar Gupta

1
Buena solución, fácil de seguir. No incluí el mío en una función, así que para resolver al final en lugar de ponerlo return sequence;pusesequence.then(() => { do stuff });
Joe Coyle

25

Util simple para la promesa estándar de Node.js:

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

ACTUALIZAR

items-promise es un paquete NPM listo para usar que hace lo mismo.


66
Me encantaría ver esto explicado con mayor detalle.
Tyguy7

Proporcioné una variación de esta respuesta con la explicación a continuación. Gracias
Zarzaparrilla

Esto es exactamente lo que hago en entornos anteriores al Nodo 7 que no tienen acceso a async / wait. Bonito y limpio.
JHH

11

Tuve que ejecutar muchas tareas secuenciales y usé estas respuestas para forjar una función que se encargaría de manejar cualquier tarea secuencial ...

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 función toma 2 argumentos + 1 opcional. El primer argumento es la matriz en la que estaremos trabajando. El segundo argumento es la tarea en sí, una función que devuelve una promesa, la siguiente tarea se iniciará solo cuando esta promesa se resuelva. El tercer argumento es una devolución de llamada para ejecutarse cuando se hayan realizado todas las tareas. Si no se pasa la devolución de llamada, la función devuelve la promesa que creó para que podamos manejar el final.

Aquí hay un ejemplo de uso:

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

Espero que ahorre algo de tiempo a alguien ...


Increíble solución, ha sido la mejor que he encontrado en casi una semana de problemas ... Está muy bien explicado, tiene nombres internos lógicos, un buen ejemplo (podría ser mejor), puedo pedirlo con seguridad tantos veces según sea necesario, e incluye la opción de establecer devoluciones de llamada. simplemente NICE! (Sólo cambió el nombre a algo que me hace más sentido) .... recomendación para los demás ... se puede repetir un objeto utilizando 'Object.keys ( myObject )' como su 'objects_array'
DavidTaubmann

¡Gracias por tu comentario! Tampoco estoy usando ese nombre, pero quería hacerlo más obvio / simple aquí.
Salketer

5

La mejor solución que pude resolver fue con bluebirdpromesas. Simplemente puede hacer lo Promise.resolve(files).each(fs.readFileAsync);que garantiza que las promesas se resuelvan secuencialmente en orden.


1
Aún mejor: Promise.each(filtes, fs.readFileAsync). Por cierto, no tienes que hacer .bind(fs)?
Bergi

Nadie aquí parece entender la diferencia entre una matriz y una secuencia, que esto último implica un tamaño ilimitado / dinámico.
vitaly-t

Tenga en cuenta que las matrices en Javascript no tienen nada que ver con las matrices de tamaño fijo en lenguajes de estilo C. Son solo objetos con administración de clave numérica atornillada, y no tienen un tamaño o límite prescrito ( especialmente no cuando se usa new Array(int). Todo lo que hace es preestablecer el lengthpar clave-valor, afectando cuántos índices se usan durante la iteración basada en la longitud. Tiene cero efecto en la indexación o límites del índice de la matriz real)
Mike 'Pomax' Kamermans

4

Esta es una ligera variación de otra respuesta anterior. Usando promesas nativas:

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

Explicación

Si tienes estas tareas [t1, t2, t3] , entonces lo anterior es equivalente a Promise.resolve().then(t1).then(t2).then(t3). Es el comportamiento de reducir.

Cómo utilizar

Primero necesitas construir una lista de tareas! Una tarea es una función que no acepta argumentos. Si necesita pasar argumentos a su función, utilice bindu otros métodos para crear una tarea. Por ejemplo:

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

4

Mi solución preferida:

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

No es fundamentalmente diferente de otros publicados aquí, pero:

  • Aplica la función a los elementos en serie.
  • Resuelve una variedad de resultados.
  • No requiere asíncrono / espera (el soporte aún es bastante limitado, alrededor de 2017)
  • Utiliza funciones de flecha; agradable y conciso

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

Probado en Chrome actual razonable (v59) y NodeJS (v8.1.2).


3

¡Úselo Array.prototype.reducey recuerde incluir sus promesas en una función, de lo contrario ya se estarán ejecutando!

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

agradable y fácil ... debería poder reutilizar la misma semilla para el rendimiento, etc.

Es importante protegerse contra las matrices vacías o las matrices con solo 1 elemento cuando se utiliza reducir , por lo que esta técnica es su mejor opción:

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

y luego lo llaman así:

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

2

Creé este método simple en el objeto Promise:

Cree y agregue un método Promise.sequence al objeto 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) {});

Lo mejor de esta extensión del objeto Promise es que es coherente con el estilo de las promesas. Promise.all y Promise.sequence se invocan de la misma manera, pero tienen una semántica diferente.

Precaución

La ejecución secuencial de promesas no suele ser una muy buena forma de utilizarlas. Por lo general, es mejor usar Promise.all y dejar que el navegador ejecute el código lo más rápido posible. Sin embargo, existen casos de uso reales, por ejemplo, al escribir una aplicación móvil con javascript.


No, no puedes comparar Promise.ally tu Promise.sequence. Uno toma una iteración de promesas, el otro toma una serie de funciones que devuelven promesas.
Bergi

Por cierto, recomendaría evitar la promesa del constructor antipatrón
Bergi

No sabía que hacía falta un iterador. Sin embargo, debería ser lo suficientemente fácil como para reescribirlo. ¿Podría explicar por qué este es el constructor de promesas antipatrón? Leí tu publicación aquí: stackoverflow.com/a/25569299/1667011
frodeborli

@ Bergi He actualizado el código para admitir iteradores. Todavía no veo que esto sea un antipatrón. Los antipatrones generalmente deben considerarse pautas para evitar errores de codificación, y es perfectamente válido crear funciones (de biblioteca) que rompan esas pautas.
frodeborli

Sí, si lo considera una función de biblioteca, está bien, pero aún así, en este caso, un me reducegusta en la respuesta de Benjamin es mucho más simple.
Bergi

2

Puede usar esta función que se promete Lista de fábricas:

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

Promise Factory es simplemente una función simple que devuelve una Promesa:

function myPromiseFactory() {
    return somethingThatCreatesAPromise();
}

Funciona porque una fábrica de promesas no crea la promesa hasta que se le pide. Funciona de la misma manera que una función then; de hecho, ¡es lo mismo!

No desea operar sobre una serie de promesas en absoluto. Según la especificación Promesa, tan pronto como se crea una promesa, comienza a ejecutarse. Entonces, lo que realmente quieres es una variedad de fábricas prometedoras ...

Si desea obtener más información sobre Promesas, debe consultar este enlace: https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html


2

Mi respuesta está basada en 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;
    });
  };

Esta solución devuelve los resultados como una matriz como Promise.all ().

Uso:

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

2

Realmente me gustó la respuesta de @ joelnet, pero para mí, ese estilo de codificación es un poco difícil de digerir, así que pasé un par de días tratando de descubrir cómo expresaría la misma solución de una manera más legible y esta es mi tomar, solo con una sintaxis diferente y algunos comentarios.

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

Como Bergi notó, creo que la solución mejor y clara es usar BlueBird, cada uno, el código a continuación:

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

2

Primero, debe comprender que una promesa se ejecuta en el momento de la creación.
Entonces, por ejemplo, si tiene un código:

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

Necesitas cambiarlo a:

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

Entonces necesitamos encadenar las promesas secuencialmente:

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

ejecutando after(), se asegurará de que la promesa se cree (y ejecute) solo cuando llegue el momento.


1

Yo uso el siguiente código para extender el objeto Promise. Maneja el rechazo de las promesas y devuelve una serie de resultados.

Código

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

Ejemplo

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

Si lo desea, puede usar reducir para hacer una promesa secuencial, por ejemplo:

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

siempre funcionará en secuencia.


1

Usando 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 (si tiene el soporte de ES7)

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

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

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

(debes usar forloop, y noforEach porque async / await tenga problemas para ejecutarse en cada bucle)

Sin Async / Await (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
No se recomienda esperar dentro de cada uno.
Marcelo Agimóvel

@ MarceloAgimóvel - He actualizado a la solución para no funcionar forEach(según esto )
Gil Epshtain

0

Sobre la base del título de la pregunta, "¿Resolver promesas una tras otra (es decir, en secuencia)?", Podríamos entender que el OP está más interesado en el manejo secuencial de las promesas de liquidación que las llamadas secuenciales per se .

Esta respuesta se ofrece:

  • para demostrar que las llamadas secuenciales no son necesarias para el manejo secuencial de las respuestas.
  • para exponer patrones alternativos viables a los visitantes de esta página, incluido el OP si aún está interesado más de un año después.
  • a pesar de la afirmación del OP de que no quiere hacer llamadas al mismo tiempo, lo cual puede ser realmente el caso, pero igualmente puede ser una suposición basada en el deseo de un manejo secuencial de las respuestas como lo implica el título.

Si realmente no se desean llamadas concurrentes, vea la respuesta de Benjamin Gruenbaum que cubre las llamadas secuenciales (etc.) de manera integral.

Sin embargo, si está interesado (para mejorar el rendimiento) en patrones que permiten llamadas simultáneas seguidas de un manejo secuencial de las respuestas, siga leyendo.

Es tentador pensar que tiene que usar Promise.all(arr.map(fn)).then(fn)(como lo he hecho muchas veces) o un azúcar elegante de Promise lib (especialmente Bluebird), sin embargo (con crédito a este artículo ) un arr.map(fn).reduce(fn)patrón hará el trabajo, con las ventajas de que:

  • funciona con cualquier lib de promesa, incluso versiones precompatibles de jQuery, solo .then() se usa.
  • ofrece la flexibilidad de omitir el error o detener el error, lo que quieras con un mod de una línea.

Aquí está, escrito para 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 ese fragmento, Q() es específico de Q. Para jQuery, debe asegurarse de que readFile () devuelva una promesa de jQuery. Con las bibliotecas A +, las promesas extranjeras se asimilarán.

La clave aquí es la sequencepromesa de la reducción , que secuencia el manejo delreadFile promesas pero no su creación.

Y una vez que haya absorbido eso, ¡tal vez sea un poco alucinante cuando se dé cuenta de que el .map()escenario no es realmente necesario! Todo el trabajo, llamadas paralelas más manejo en serie en el orden correcto, se puede lograr reduce()solo, más la ventaja adicional de una mayor flexibilidad para:

  • Convierta de llamadas asíncronas paralelas a llamadas asíncronas en serie simplemente moviendo una línea, potencialmente útil durante el desarrollo.

Aquí está, para Qotra vez.

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

Ese es el patrón básico. Si también quisiera entregar datos (por ejemplo, los archivos o alguna transformación de ellos) a la persona que llama, necesitaría una variante leve.


No creo que sea una buena idea responder preguntas contrarias a las intenciones de los OP ...
Bergi

1
Esto sequence.then(() => filePromise)es un antipatrón: no propaga errores tan pronto como podrían (y crea unhandledRejectionen libs que los admiten). Prefieres usar Q.all([sequence, filePromise])o $.when(sequence, filePromise). Es cierto que este comportamiento puede ser lo que desea cuando intenta ignorar u omitir errores, pero al menos debe mencionar esto como una desventaja.
Bergi

@ Bergi, espero que el OP intervenga y juzgue si esto es realmente contrario a sus intenciones o no. Si no, eliminaré la respuesta, supongo, mientras tanto, espero haber justificado mi posición. Gracias por tomarlo lo suficientemente en serio como para proporcionar comentarios decentes. ¿Puede explicar más sobre el antipatrón o proporcionar una referencia por favor? ¿Se aplica lo mismo al artículo donde encontré el patrón básico ?
Roamer-1888

1
Sí, la tercera versión de su código (que es "tanto paralela como secuencial") tiene el mismo problema. El "antipatrón" necesita un manejo sofisticado de errores y es propenso a adjuntar controladores de forma asincrónica, lo que provoca unhandledRejectioneventos. En Bluebird puede solucionar este problema utilizando el sequence.return(filePromise)que tiene el mismo comportamiento pero maneja los rechazos de manera correcta. No conozco ninguna referencia, solo se me ocurrió, no creo que el "(anti) patrón" tenga un nombre todavía.
Bergi

1
@Bergi, se puede ver claramente algo que no puedo :( Me pregunto si este nuevo necesidades anti-patrón que se documentarán en alguna parte?
Roamer-1888

0

Su enfoque no es malo, pero tiene dos problemas: se traga los errores y emplea el antipatrón de construcción de promesa explícita.

Puede resolver estos dos problemas y hacer que el código sea más limpio, mientras sigue empleando la misma estrategia general:

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

Si alguien más necesita una forma garantizada de una forma ESTRICTAMENTE secuencial de resolver Promesas al realizar operaciones CRUD, también puede usar el siguiente código como base.

Siempre que agregue 'return' antes de llamar a cada función, describiendo una Promesa, y use este ejemplo como base, la siguiente llamada a la función .then () comenzará CONSISTENTEMENTE después de completar la anterior:

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

El método push y pop de matriz se puede usar para la secuencia de promesas. También puede impulsar nuevas promesas cuando necesite datos adicionales. Este es el código que usaré en el cargador React Infinite para cargar la secuencia de páginas.

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 mayoría de las respuestas no incluyen los resultados de TODAS las promesas individualmente, por lo que en caso de que alguien esté buscando este comportamiento en particular, esta es una posible solución mediante la recursividad.

Sigue el estilo de Promise.all:

  • Devuelve la matriz de resultados en la .then()devolución de llamada.

  • Si alguna promesa falla, se devuelve inmediatamente en la .catch()devolución de llamada.

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 sobre la tasksdeclaración de matriz :

En este caso no es posible usar la siguiente notación como Promise.allusaría:

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

Y tenemos que usar:

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

La razón es que JavaScript comienza a ejecutar la promesa inmediatamente después de su declaración. Si usamos métodos como Promise.all, solo verifica que el estado de todos ellos sea fulfilledo rejected, pero no inicia la exención en sí. Usando () => promise()detenemos la ejecución hasta que se llame.


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

Aquí la clave es cómo llamar a la función dormir. Debe pasar una serie de funciones que en sí devuelve una promesa en lugar de una serie de promesas.


-1

Esto es para ampliar cómo procesar una secuencia de promesas de una manera más genérica, admitiendo secuencias dinámicas / infinitas, basadas en la implementación de secuencia spex :

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

No solo esta solución funcionará con secuencias de cualquier tamaño, sino que puede agregarle fácilmente aceleración de datos y equilibrio de carga .

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.