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

Resuelto XåpplI'-I0llwlg'I - asked hace 10 años • 36 respuestas

Considere el siguiente código que lee una serie de archivos de forma serial/secuencial. readFilesdevuelve una promesa, que se resuelve sólo una vez que se han leído todos los archivos 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 with the first file!
  });
};

El código anterior funciona, pero no me gusta tener que recurrir a la recursividad para que las cosas ocurran de forma secuencial. ¿Existe una forma más sencilla de reescribir este código para no tener que usar mi readSequentialfunción extraña?

Originalmente intenté usar Promise.all, pero eso provocó que todas las readFilellamadas ocurrieran simultáneamente, que no es lo que quiero:

var readFiles = function(files) {
  return Promise.all(files.map(function(file) {
    return readFile(file);
  }));
};
XåpplI'-I0llwlg'I - avatar Jul 05 '14 18:07 XåpplI'-I0llwlg'I -
Aceptado

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

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

Si lo desea, puede posponer 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 reducir:

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

En otras bibliotecas de promesas (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 await hoy.

Benjamin Gruenbaum avatar Jul 05 '2014 11:07 Benjamin Gruenbaum

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 inmediatamente, no podemos simplemente crear una serie de promesas, todas se ejecutarían en paralelo.

En lugar de ello, necesitamos crear una serie de funciones que devuelvan una promesa. Luego, cada función se ejecutará secuencialmente, lo que luego inicia la promesa interna.

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

Se vuelve un poco complicado usarlo reduceen combinación con promesas, por lo que he dividido la línea en algunos bocados más pequeños y digeribles a continuación.

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

Esta promesa luego se pasará al reducemétodo como promise. Ésta es la clave para encadenar cada promesa de forma secuencial. La siguiente promesa a ejecutar es funcy cuando se thenactiva, los resultados se concatenan y luego se devuelve esa promesa, ejecutando el reduceciclo con la siguiente función de promesa.

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

Ejemplo de ES6 (una línea)

/*
 * 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 de 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))
joelnet avatar Dec 13 '2016 06:12 joelnet