Agos 17, 2019

Aprende a manejar funciones asíncronas en JavaScript


Una de las cosas que hacen de JavaScript un lenguaje impresionante, es la capacidad de realizar tareas asíncronas. ¿Asíncronas? Si, JavaScript a pesar de ser un lenguaje que procesa en un solo hilo puede seguir su ejecución sin importar que la función anterior no haya completado.

¡No tan rápido Chaval! esta capacidad solo es posible con algunas funciones. Veamos un pequeño ejemplo.

    function sum(x,y) {
        console.log(x + y)
    }

    setTimeout(function () {
        sum(2,2)
    },1000)

    sum(4,4)

¿Cuál de las dos sumas se mostrará primero? ¿Qué es setTimeout? ¿Por qué sigo haciéndome éstas preguntas? xd

Ok, si ya desesperaste y probaste en la consola de tu navegador, te habrás dado cuenta que la 8 fue mostrado primero que 4. Esto se debe a que setTimeout es una función asíncrona que recibe una función como primer parámetro y la cantidad de mili segundos que será ejecutada dicha función.

¿Pero qué tal si lo cambiamos a 0 el segundo parámetro de setTimeout?

    function sum(x,y) {
        console.log(x + y)
    }

    setTimeout(function () {
        sum(2,2)
    },0)

    sum(4,4)

¡Voila! obtenemos el mismo resultado ¿Cómo es esto posible?

La magia del Event Loop

Para entender como funciona todo este concepto, debemos tomar en cuenta que JavaScript de manera nativa no cuenta con cosas como setTimeout, sino que son API's suplidas por su entorno de ejecución tales como el Navegador o otros como Node.js.

El Event Loop, explicado de una manera simplista, es el que se encarga de manejar los eventos que serán ejecutados, de acuerdo a su naturaleza.

Describiendo lo que ocurre en la imagen:

Vemos que el entorno de JavaScript esta compuesto por:

  • El Memory Heap: que es el uso de memoria que utiliza el lenguaje para sus objetos.
  • El Call Stack:: que es la pila donde se agregan al contexto el código que es ejecutado.

Fuera de esto, tenemos las Web API's (que también pueden llamarse API's del entorno de ejecución). También el Callback Queue o Cola que son las funciones que serán procesadas.

Por último, el Event Loop es quien se encarga de mover las funciones de la Cola al Call Stack.

Veamos un ejemplo:

    console.log("hola")
    
    setTimeout(function timeout() {
        console.log("mundo")
    }, 5000)
    
    console.log("Ubykuo everywhere, everywhere")

https://medium.com/@ubykuo/event-loop-la-naturaleza-asincr%C3%B3nica-de-javascript-78d0a9a3e03d

Vemos que la función timeout() es procesada por el Web API . Luego de estar completada pasa a la Cola para al final ser ejecutada luego de que ya ha sido mostrado el último console.log.

Pues... ¿cómo lo manejamos?

¿Qué sucede si nosotros necesitamos esperar o ejecutar luego de que una función asíncrona se complete?

Existen 2 formas para manejar este tipo situaciones. Aunque este apartado merece su propio post, mostraremos algunas de las formas en las que se manejan este tipo de situaciones.

Callbacks

Los callbacks son funciones que son pasadas como parámetro para ser ejecutadas luego de ciertas operaciones.

    function saludar(nombre) {
        console.log("Hola " + nombre)
    }
    
    function recibirInvitadoJuan(callback) {
        var invitado = "Juan"
        callback(invitado)
    }

    recibirInvitadoJuan(saludar) // Hola Juan

Su uso en las funciones asíncronas sería el siguiente:

    function saludar(nombre) {
        console.log("Hola " + nombre)
    }
    
    function recibirJuanMasTarde(callback) {
        setTimeout(function () {
            var invitado = "Juan"
            callback(invitado)
        },5000)     
    }

    recibirJuanMasTarde(saludar)
    // ... (Espera por 5 segundos)
    // Hola Juan

Aunque esta forma a primera vista parece sencilla, puede verse afectada cuando se necesitan anidar muchas funciones, a este problema se le llama Callbacks Hell

Promises

Promise o (Promesa) es un objeto en JavaScript que representa un valor que puede o no estar disponible en el futuro. Fueron implementadas para resolver (de una manera mejor estructurada) las tareas asíncronas que se realizaba con los Callbacks.

Las promesas son creadas con el keyword 'new' y pasando un callback que recibe dos parámetros que a su vez también son funciones, que serán llamadas si la operación tuvo éxito (resolve) y si algo falló (reject).

Veamos un ejemplo:

    function sumarMasTardeMostrar(x,y) {
        var nuevaPromesa = new Promise(function (resolve, reject) {
            if (!x || !y) {
                reject("Falta un numero")
            }
            
            setTimeout(function () {
                resolve(x+y)
            },1000)
        }) 
        return nuevaPromesa
    }

    sumarMasTardeMostrar(5,3)
        .then(function (resultado){
            console.log(resultado)
        })
        .catch(function (error) {
            console.error(error)
        })

Luego que tenemos esta función asíncrona, utilizamos then cuando obtenemos el resultado y catch para capturar los errores.

Las ventajas de las promises es que ya muchas de las nuevas funcionalidades asíncronas que se le brinda al lenguaje, retornan una promise por defecto. Este es el caso de fetch, que es provisto por los navegadores para hacer peticiones HTTP.

    const url = "https://example.com/api/users"
    const body = {
    //  ...
    }
    
    fetch(url, body)
        .then(function (response) {
            return response.json()
        })
        .then(function (jsonResponse) {
            return console.log(jsonResponse)
        })
        .catch(function (error) {
            console.error(error)
        })

Aquí vemos que además de fetch también Body.json utiliza promise. Este último es el objeto resultante de la petición, que a su vez utiliza este método para convertir la respuesta objeto JSON.

Async - Await

Por último, tenemos este método, introducido como standard en la especificación de ECMA 2017. Al igual que then y catch éstas trabajan con Promise, solo que de una manera muy particular y haciendo que la sintaxis sea más legible.

    function async getUsers () {
        const url = "https://example.com/api/users"
        const body = {
        //  ...
        }
            
        const response = await fetch(url, body)
        const jsonResponse = await response.json()
    }

Lo primero que debemos hacer crear una función con el keyword asycn delante. Así podremos utilizar el await dentro de esta función. El await debe ser utilizado delante de la sentencia que nos devolverá la promesa. Asi en vez de obtener el objeto Promise, obtendremos el valor esperado.

Conclusión

Debemos tomar en cuenta que JavaScript tiene funciones que no retornaran su valor de forma inmediata y aprender a utilizar este comportamiento a nuestro favor. Manejar este conocimiento nos da muchas alternativas y la posibilidad de utilizar todas las capacidades del lenguaje.

¿Qué opinas?

Aprende a manejar funciones asíncronas en JavaScript
Commentarios