¿Resolver promesas una tras otra (es decir, en secuencia)?
Considere el siguiente código que lee una serie de archivos de forma serial/secuencial. readFiles
devuelve 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 readSequential
función extraña?
Originalmente intenté usar Promise.all
, pero eso provocó que todas las readFile
llamadas ocurrieran simultáneamente, que no es lo que quiero:
var readFiles = function(files) {
return Promise.all(files.map(function(file) {
return readFile(file);
}));
};
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.
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 reduce
en 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 reduce
con un valor inicial de Promise.resolve([])
o una promesa que contenga una matriz vacía.
Esta promesa luego se pasará al reduce
método como promise
. Ésta es la clave para encadenar cada promesa de forma secuencial. La siguiente promesa a ejecutar es func
y cuando se then
activa, los resultados se concatenan y luego se devuelve esa promesa, ejecutando el reduce
ciclo 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))