Cómo hacer peticiones por Ajax que eviten callback hells

13-4-2020

El problema con los callbacks, es que si simplemente llamamos a las funciones, y como la página sigue cargando, las mismas pueden ser devueltan en forma desordenada:

//obtenemos un listado de noticias de ejemplo que es un string listo para ser jsonificado
let  url = 'https://5c8ef17a3e557700145e85c7.mockapi.io/noticias/'

Si hacemos simplemente los pedidos, nos vienen en un orden aleatorio:

function getNoticia(id) {
    generamos un nuevo objeto de conexión ajax
    let xhr = new XMLHttpRequest
    //abrimos la conexión usando el método get y le agregamos al final el número de id que queremos mostrar, esto dependerá de cómo esté estructurada la URL de donde sacamos la data
    xhr.open('get',url+id)
    //agregamos el escuchador que hará algo al cargarse el pedido
    xhr.addEventListener('load', () => {
        //si la respuesta es 200
        if(xhr.status == 200) {
            //metemos la respuesta, que es un string en formato JSON, en una variable, parseándola, es decir, la variable contendrá un JSON
            let respuesta = JSON.parse(xhr.response)
            console.log(respuesta)
        }
    })
    //enviamos todo
    xhr.send()
}


//llamamos a getNoticia() y le pedimos las diez primeras noticias... pero no necesariamente nos vienen en el orden que aparece aquí
getNoticia(1)
getNoticia(2)
getNoticia(3)
getNoticia(4)
getNoticia(5)
getNoticia(6)
getNoticia(7)
getNoticia(8)
getNoticia(9)
getNoticia(10)

 

Cómo podríamos hacer el pedido generando un pequeño infierno? (aka. antes de de JS6):

//agregamos otro parámetro, el callback
function getNoticia(id,cb) {
    let xhr = new XMLHttpRequest
    xhr.open('get',url+id)
    xhr.addEventListener('load', () => {
        if(xhr.status == 200) {
            let respuesta = JSON.parse(xhr.response)
            console.log(respuesta)
            //si hay callback, ejecutar el callback...
            if(cb) cb(respuesta)
        }
    })
    xhr.send()
}

//y aquí se va todo a un lugar que no está bueno... OH!

getNoticia(1, () => {
    getNoticia(2, () => {
        getNoticia(3, () => {
            getNoticia(4, () => {
                getNoticia(5, () => {
                    getNoticia(6, () => {
                        getNoticia(7, () => {
                            getNoticia(8, () => {
                                getNoticia(9, () => {
                                    getNoticia(10)
                                })
                            })
                        })
                    })
                })
            })
        })
    })
})

 

Promesas al Rescate

Esta solución es espantosa, por lo que JS6 vino a dar un poco de orden a esto, con las Promesas. Nada cambia en la forma en la qu ese resuelven las cosas, pero es más clara la generación y posterior lectura del código:

//arrancamos con la función sólo con el primer parámetro, como al principio
function getNoticia(id) {
    //esta función nos devolverá un objeto del tipo Promesa
    //la promesa tiene dos parámetros, la respuesta si se resuelve bien, y el rechazo si se resuelve mal, es decir, no trae lo que promete.
    return new Promise((resolve,reject) => {
        //y aquí vemos cómo dentro de la promesa, hacemos casi lo mismo...
        let xhr = new XMLHttpRequest
        xhr.open('get',url+id)
        xhr.addEventListener('load', () => {
            if(xhr.status == 200) {
                let respuesta = JSON.parse(xhr.response)
                console.log(respuesta)
                //al final de la promesa, si el status es 200, usamos resolve y metemos como parámetro, la respuesta que parseamos
                resolve(respuesta)
            } //si el status no es 200...
            else {
                let error = {
                    type: 'Error Status',
                    code: xhr.status
                } //metemos en reject, el segundo parámetro de la promesa, el error...
                reject(error)
            }
        })
        //agregamos otro escuchador al error de ajax
        xhr.addEventListener('error', e => {
            let error = {
                type: 'Error Ajax',
                code: e
            }
            reject(error)
        })
        //y como siempre, enviamos
        xhr.send()
    })
}

Con la promesa implementada, tenemos dos formas de llamar a getNoticia() por orden:

THEN y CATCH

/*
por ejemplo metiendo todos los getNoticia en una función que los traiga por orden con .then() Dentro de then, va como parámetro la función que se ejecutará luego de la función que viene antes de ese then... entones todo funciona con then() concatenados...

function getNoticias() {
            getNoticia().then().then() y así....
            .catch()
}
*/
function getNoticias() {
    getNoticia(1)
    .then(() => getNoticia(2))
    .then(() => getNoticia(3))
    .then(() => getNoticia(4))
    .then(() => getNoticia(5))
    .then(() => getNoticia(6))
    .then(() => getNoticia(7))
    .then(() => getNoticia(8))
    .then(() => getNoticia(9))
    .then(() => getNoticia(10))
    //si algo tira error, lo metemos aquí
    .catch( error => console.log('ERROR:', error) )
}
//y después simplemente llamamos a getNoticias()
getNoticias();

ASYNC y AWAIT

Una forma fácil de hacer lo mismo que con .then() es con await. Y en lo personal me parece más sencilla de leer. Recordemos que aquí sólo se trata de hacer más legible el código. El código, al fin y al cabo, sigue haciendo lo mismo.

/*
La estructura es 
   async function getNoticias() {
         try{
              await getNoticia()
              await getNoticia()
         }
         catch() {
         }
   }
*/
//usamos async antes de la función para avisar que se trata de una función asincrónica y que tendremos una serie de await adentro....
async function getNoticias() {
    try {
        await getNoticia(1)
        await getNoticia(2)
        await getNoticia(3)
        await getNoticia(4)
        await getNoticia(5)
        await getNoticia(6)
        await getNoticia(7)
        await getNoticia(8)
        await getNoticia(9)
        await getNoticia(10)
    }
    catch(error) {
        console.log('ERROR:', error) 
    }
}

//y después simplemente llamamos a getNoticias()
getNoticias();

Como vemos, ambas formas son bastante parecidas, y el resultado es exactamente el mismo. Termina siendo cuestión de preferencia.