🖖 Inicio

SERVICIOS

Salir

nochedía

Combinando promesas (await async) con loops en JavaScript

600 palabras
3 minutos
February 13, 2020
blogjavascript

Los loops `forEach` y `map` no funcionan como los clásicos `for`, con lo que su uso con `await` y `async` se complica

  1. Promesas
  2. Entonces, y cuando hacemos loops qué, lo mismo no?
  3. Reduce

Promesas

Las promesas en JavaScript fueron un gran qué

Pero eran un pequeño caos

El objetivo? Conseguir una manera de no bloquear el código mientras una instrucción aún no se había completado

Por ejemplo, leer datos de una fuente externa

Porque no podíamos esperarnos eternamente, teníamos que dejar hacer y nosotros seguir con lo nuestro

Por lo que nada, esta función nos devolvía una promise, y cuando ésta se resolvía entonces ya veíamos lo que hacíamos

Esto era perfecto

Pero ... y si querías que se bloquease el código en esa instrucción?

Entonces necesitábamos tirar de .then() con estructuras largas y nada intuitivas

Estructuras ofuscadas

Luego vino el async y el await, una manera de reescribir promesas que francamente ha simplificado (y mucho) nuestra vida

Por ejemplo, fíjate en esta función

Queremos que data sea algo que sacamos de algún sitio remoto, pero cuando la leemos con el console.log nos sale un pending

js
const fetching = () => {
const data = fetchMyData()
console.log(data) // me sale "pending"
}
fetching()

El pending ya nos dice lo que nos dice, que la promise que hemos recibido está pendiente de resolverse, pero qué hacemos? Cómo podemos esperar?

Tan sencillo como esto

js
const fetching = async () => {
const data = await fetchMyData()
console.log(data) // data es data, lo que sea que fuere data
}
fetching()

Es tan diáfano que casi uno llora de la emoción

Detrás de este syntactic sugar hay la estructura típica de las promises, pero puedes juzgar tú mismo la pinta que tendría

js
const fetching = async () => {
fetchMyData().then(data => {
console.log(data)
})
}
fetching()

Bien

Ahora imagínate la pinta que tendría con muchas promesas implicadas

Entonces, y cuando hacemos loops qué, lo mismo no?

Y no

😀

Porque hay loops y loops

Por ejemplo, esto sí funciona, y de paso le pongo funciones para poder probar el ejemplo en un jsfiddle o similar

js
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']
for (const url of urls) {
const data = await fetchUrl(url)
console.log(data)
}
}
fetching()

Perfecto, a cada segundo nos sale el console.log que queremos

El problema, que este loop no es muy funcional (en el sentido de programación funcional), por lo que posiblemente preferiremos utilizar estructuras más modernas, como el forEach

Ningún problema

js
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']
urls.forEach(url => {
const data = await fetchUrl(url)
console.log(data)
})
}
fetching()

Pero esto ya nos da un error

Por qué? Porque el loop forEach nos ejecuta una función, por lo que el async tenemos que moverlo

js
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()

Ahora sí

Pero ... funciona? Pues no! Porque pasa 1 segundo y de repente nos salen 3 console.log a la vez

Y esto por qué?

Porque el loop forEach ha lanzado las funciones sin esperar (las 3 funciones que son las tres tandas del loop)

Luego cada función ha esperado sus 1000 milisegundos, pero lo han hecho en paralelo

Y otro problema es que tampoco se espera a seguir la ejecución del propio forEach

Por ejemplo

js
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)
})
console.log('hemos acabado') // sale al principio!
}
fetching()

Aquí el hemos acabado nos aparece inmediatamente, y después nos aparecen los tres fantastic ... a la vez

Podríamos pensar que esto quizá funcionaría

js
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.forEach(async url => {
const data = await fetchUrl(url)
console.log(data)
})
console.log('hemos acabado')
}
fetching()

Pero ya vemos que no, y esto es porque nadie controla qué es lo que hace forEach

Podríamos controlarlo con map()

A diferencia de forEach, map nos devuelve un valor y resulta que ese valor es una promesa

js
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']
const promesas = urls.map(async url => {
const data = await fetchUrl(url)
console.log(data)
// lleva implícito un return new Promise...
})
await Promise.all(promesas)
console.log('hemos acabado')
}
fetching()

De este modo conseguimos que finalmente el hemos acabado nos salga al final

Pero ... el map() sigue ejecutando las funciones en paralelo, porque funciona exactamente igual que el forEach()

Entonces, hay alguna forma de hacerlo funcional? O mejor me quedo con el for..of del principio?

Depende

Para mi, el for..of se lee inmediatamente y no resulta difícil de entender

Hay una alternativa que no es imperativa sino declarativa, pero al contrario que con el for tiene fama de oscura y complicada

Si eres un mago de JavaScript seguro que no tendrás problemas

En mi caso tengo que admitir que tengo sentimientos encontrados con esto, y he leído gente que aboga por evitar su uso si no es indispensable

Y esto lo dicen porque cuesta de entender, y si tu eres un mago del JS quizá el que tenga que leer tu código no lo es, y allí estás causando fricción

Y cuál es la alternativa? reduce()

Reduce

js
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
return fetchUrl(url).then(data => console.log(data))
}, Promise.resolve())
console.log('hemos acabado')
}
fetching()

Y así funciona a la perfección, justo como el ejemplo con for..of

Pero si comparas el código de ambas opciones ...

IMO y en este caso concreto, no termino de ver que las ventajas de escribir de un modo más declarativo, porque el código se complica quizás demasiado

Eso sí, si abrazas el uso de reduce en todo su esplendor, se te abren las puertas al currying, algo de lo que quiero hablar en un futuro cercano

Y cómo solucionas el código de arriba?

Como siempre, encapsulandolo todo

js
const wait = ms => new Promise((r, j) => setTimeout(r, ms))
const fetchUrl = async url => {
await wait(1000)
return 'fantastic ' + url
}
const grab = (urls, f) =>
urls.reduce(async (previousPromise, url) => {
await previousPromise
return fetchUrl(url).then(f)
}, Promise.resolve())
const debug = data => console.log(data)
const fetching = async () => {
const urls = ['https://www.site1.com', 'https://www.site2.com', 'https://www.site3.com']
await grab(urls, debug)
console.log('hemos acabado')
}
fetching()

Que podríamos condensar más

js
const wait = ms => new Promise((r, j) => setTimeout(r, ms))
const fetchUrl = async url => wait(1000).then(() => 'fantastic ' + url)
const grab = (urls, f) =>
urls.reduce(async (previousPromise, url) => previousPromise.then(() => fetchUrl(url).then(f)), Promise.resolve())
const debug = data => console.log(data)
const fetching = async () => {
const urls = ['https://www.site1.com', 'https://www.site2.com', 'https://www.site3.com']
await grab(urls, debug)
console.log('hemos acabado')
}
fetching()

E incluso podríamos hacer currying

Pero como digo, tengo mis dudas acerca de la conveniencia de utilizar reduce() aquí

Qué tal la entrada?
👌 Bien 🙌🙌
👍 Bien, pero algunas cosas podrían explicarse mejor 😬
🤷‍♂️ Da por sentadas demasiadas cosas 😒
🤷‍♂️ A ver, hay poca chicha 😬
🤷‍♂️ Los ejemplos no son muy claros 🙇‍♂️
🤷‍♂️ No se entiende, está mal escrito 👎
✍️ Hay errores, revísalo en cuanto puedas 🙏

Newsletter de kuworking, un correo de tanto en cuanto

Quizá te interese

Privacidad
by kuworking.com
[ 2020 >> kuworking ]