Combinando promesas (await async) con loops en JavaScript
Los loops forEach
y map
no funcionan como los clásicos for
, con lo que su uso con await
y async
se complica
Promesas con await y async
Las promesas en JavaScript fueron un gran qué, pero eran y son un pequeño caos
El objetivo? Conseguir una manera de no bloquear la ejecución del código mientras una instrucción aún no se había completado
- Por ejemplo, leer datos de una fuente externa
Cómo funcionan las promesas? Nos devuelven una promise
y una manera de saber cuándo esa promesa se resuelve
El concepto es perfecto, pero en código la implementación no fue brillante
Entonces vino el async
y el await
, una manera de expresar promesas que sí es brillante
Por ejemplo, vamos a suponer que aquí la función fetchMyData
nos devuelve una promesa
const fetching = () => {
const data = fetchMyData()
console.log(data) // me sale "pending"
}
fetching()
En este código necesitamos esperar a que data
exista (es decir a que la promesa que nos devuelve fetchMyData
se resuelva)
Pues tan sencillo como esto
const fetching = async () => {
const data = await fetchMyData()
console.log(data) // data es lo que sea que fuere data
}
fetching()
Y cuando estamos en un loop qué, funciona igual?
Para los loops "clásicos" la respuesta era sí, funcionan igual
Por ejemplo con un for..of
// función que implementa un wait con promesas
const wait = ms => new Promise((r, j) => setTimeout(r, ms))
// función que emula un proceso asíncrono
const fetchUrl = async url => {
await wait(1000)
return 'fantastic ' + url
}
// aquí vamos a hacer ver que pedimos 3 webs, y lo hacemos con un loop for .. of
const fetching = async () => {
const urls = ['https://www.site1.com', 'https://www.site2.com', 'https://www.site3.com']
for (const url of urls) {
const data = await fetchUrl(url)
console.log(data)
}
}
fetching()
En este loop las cosas funcionan como queremos, un console.log
a cada segundo, fabuloso
Pero y si queremos un loop menos declarativo y más funcional?
Por loops funcionales tenemos el forEach
y el map
, y estos loops no funcionan como esperaríamos
La diferencia entre un forEach
y un map
es que el segundo nos devuelve un valor, y el primero no
Vamos a verlo con un forEach
, y verás que he puesto el async
dentro de la función forEach, eso ya te da una pista de lo que está ocurriendo
const wait = ms => new Promise((r, j) => setTimeout(r, ms))
const fetchUrl = async url => {
await wait(1000)
return 'fantastic ' + url
}
const fetching = () => {
const urls = ['https://www.site1.com', 'https://www.site2.com', 'https://www.site3.com']
urls.forEach(async url => {
const data = await fetchUrl(url)
console.log(data)
})
}
fetching()
Funciona como esperaríamos?
No
Tenemos los console.log
que aparecen al instante y no a cada segundo
Por qué?
Porque todos los console.log
se ejecutan a la vez
Por qué?
forEach
y map
ejecutan una función que recibe como argumento el elemento del array
Array.forEach((argumentos) => {})
Bien, pues el problema es que esas funciones las ejecuta en paralelo, no podemos decirle que se espere
Y además, tampoco podemos bloquear el código de todo el forEach
completo
Lo puedes comprobar en este ejemplo
const wait = ms => new Promise((r, j) => setTimeout(r, ms))
const fetchUrl = async url => {
await wait(5000) // ahora son 5 segundos
return 'fantastic ' + url
}
const fetching = () => {
const urls = ['https://www.site1.com', 'https://www.site2.com', 'https://www.site3.com']
urls.forEach(async url => {
const data = await fetchUrl(url)
console.log(data)
})
console.log('hemos acabado') // sale al principio!
}
fetching()
Aquí el mensaje hemos acabado
nos aparece inmediatamente, y a los 5 segundos nos aparecen los tres fantastic ...
a la vez
Es decir, aquí no se bloquea nada
Una opción podría ser ponerle un await
delante del forEach
, pero tampoco funciona puesto que el forEach no devuelve ninguna promesa
Si en lugar de un forEach
utilizamos map
, éste nos devuelve una promesa por cada elemento, y podríamos hacer lo siguiente
const fetching = async () => {
const urls = ['https://www.site1.com', 'https://www.site2.com', 'https://www.site3.com']
const promesas = urls.map(async url => {
const data = await fetchUrl(url)
})
await Promise.all(promesas)
console.log('hemos acabado')
}
fetching()
Así conseguimos bloquear el código hasta que termine todo el map
, pero no podemos bloquear cada iteración del loop (ocurren en paralelo)
Cómo podemos hacerlo?
Con reduce
(tienes una entrada aquí)
Loop con reduce
const wait = ms => new Promise((r, j) => setTimeout(r, ms))
const fetchUrl = async url => {
await wait(1000)
return 'fantastic ' + url
}
const fetching = async () => {
const urls = ['https://www.site1.com', 'https://www.site2.com', 'https://www.site3.com']
await urls.reduce(async (previousPromise, url) => {
await previousPromise
const data = await fetchUrl(url)
console.log(data)
return Promise.resolve()
}, Promise.resolve())
console.log('hemos acabado')
}
fetching()
reduce
nos permite incrustar una función en cada ciclo del loop, por lo que podemos implementar una promesa y esperar a que se resuelva, y así bloquear el código como lo hacíamos en el for..of
anterior
Y ya lo tenemos
Dicho esto, mejor con reduce
o mejor con for..of
?
Depende de tu caso
-
Bien podrías definir una función global con
reduce
, refactorizarla y reutilizarla y así poder disfrutar del paradigma funcional -
O podrías utilizar
for..of
y quedarte tan contento, no hay nada malo en mezclar paradigmas
🙋♂️
Lista de correo: escribo algo de tanto en cuanto