DÍA 21 / 2017

La programación asíncrona es la reina de la selva

"Programación asíncrona", "promesas", "event loop", "bloquear el hilo principal". A medida que vayamos queriendo hacer UIs mejores y más complejas, nos iremos encontrando con estos animales de la jungla de JS a los que es mejor entender, para no subestimarlos.


En el corazón del navegador, a medio camino entre la red (network) y las WebApis, está la isla de "Javascript Land". Cuya topografía está cubierta principalmente por una selva; en donde, principalmente, las leyes de la selva se cumplen: los animales más grandes, bloquean el hilo principal.

Obviously

No voy a entrar en harina de explicar la historia de la programación asíncrona en Javascript. Algunos/as habrán usado $.ajax u otras librerías y puede que la sintaxis de las promesas les resulte familiar y otros/as estarán usando Babel.js desde hace milenios; otr@s hace unos meses ni teníamos la noción de que existían. Por eso quería compartir lo que he aprendido. Y para ello, empecemos por el principio: qué es una promesa:

Es un objeto que puede devolver un valor en algún momento del futuro: ya sea una respuesta de OK o la razón por la que no hay respuesta (por ejemplo, un fallo de red).

Para saber más sobre el tema, dejo un par de enlaces:

  1. Promise [MDN]
  2. Master the JavaScript Interview: What is a Promise?

La primera es la documentación de MDN y la segunda, un artículo de _ericelliott.

Tenemos que tener claro que cuando hablamos de código asíncrono, hablamos de código no bloqueante; por lo general. Por eso, tenemos que conocer bien las alternativas a la sincronía (AKA, bloqueante).

Las promesas no son el único patrón asíncrono que hay en JS. Y para entenderlas bien, primero deberíamos hablar de lo que es el "Event Loop" y los "callbacks". Voy a matar 2 pájaros de un tiro con el siguiente ejemplo (un método asíncrono con un callback):

setTimeout(() => console.log('async!', 'a'));
Nota:
Para el/la que no esté muy acostumbrado a la sintaxis de ES6, he utilizado una "Arrow function". Es muy minimalista:

Si simplemente queremos hacer una función anónima que devuelve un "statement":

function() {
  return X;
}

Sería lo mismo que hacer: (Si solo tenemos un "statement", podemos obviar los {})

() => X; // cool

Incluso se puede eliminar el ; para los más puristas...

Volviendo al ejemplo del setTimeout..., ¿por qué lo ponen siempre como ejemplo de asincronía?

En el momento en el que se llama, no es cuando se ejecuta.

Incluso no habiéndole pasado el segundo parámetro a setTimeout (que espera el delay en milisegundos), en vez de ponerse a la pila de llamadas (stack), se pone en otra pila (otro stack), que es la Cola (Queue).

Aquí está la clave de que no bloqueen el hilo principal, que en JS es el hilo único.

Callbacks

¿Qué es lo que se añade a la cola? Nuestro callback.

Un callback es una función que se pasa como parámetro, comúnmente, para controlar que se ejecute al acabar otro proceso.

¿Cuál es nuestro callback en este ejemplo?

() => console.log('async!', 'a') // Esta parte (la función, que se pasa como único parámetro al `setTimeout`)

Aunque le hayamos dicho que se ejecute tras cero retraso (null || 0); el resto de cosas que tuviéramos en nuestro scope de la función (al mismo nivel que setTimeout), se ejecutarán antes. Y hasta que no se hayan vaciado todas las llamadas en la pila (stack), y se haya consumido el tiempo marcado por el setTimeout (en este caso, 0), no se añade nuestro console.log('async!', 'a') a la pila y se ejecuta.

Para ilustrar el ejemplo, he preparado un pen:

See the Pen Async Example by Brav0 (@brav0) on CodePen.dark

Aunque pusiéramos 100000 en el while loop, no se pintaría en consola "async!" "a", hasta no pintarse "b" 100000.

El "Event loop"

El "Event loop", es esa maquinita que se encarga de llenar el stack con las llamadas que hemos delegado al "modelo de concurrencia" (a la cola), una vez el stack ha sido despejado (se han ejecutado todas nuestras funciones del mismo scope).

Para visualizarlo, tenemos en la herramienta Loupe, creada por Philip Roberts, quien en su charla, "What the heck is the event loop anyway?" (JSConf EU 2014) explicaba muy bien el funcionamiento de estos bichejos!

Recapitulando:

  • Hemos visto qué es el "Event Loop" (a la lejanía, con prismáticos).
  • Qué es el stack de llamadas (ladera abajo, junto a un río).
  • Qué es un callback (galopando por los desfiladeros stack y queue).

La próxima vez que queráis explicar asincronía a un/a junior o si queréis poner a prueba a un/a senior jejeje, podéis tirar del pen que os pasaba. 😜

Siguiendo por la travesía de la asincronía, sé que os he prometido que veríamos "Promesas", pero que sepáis que no son para nada los animales "más guays" que tenemos en "Javascript Land" de momento. Las funciones async/await como patrón asíncrono, function generators, o la fetch API, como sustituta de XMLHttpRequest son de mis animales asíncronos favoritos. Aunque de momento, tengamos que confiar en polyfills o librerías (para nuestro amigo IE, entre otros):

Promesas

Bueno, sin más dilación, lo que os había prometido (:facepalm:), vamos ello: Promesas!! 🎉

Las promesas nativas siguen la especificación de Promises/A+. Una vez invocadas, se encontrarán en 1 de 3 estados posibles:

  • Cumplida
  • Rechazada
  • Pendiente

En realidad, no son nada nuevo de hoy en día: surgieron a finales de los años 80! [1]

Pero, ¿qué tan diferentes son al patrón de callbacks?

En vez de devolver un simple callback, se devuelve otra promesa (cada then, finally, catch, ...). Así, le damos un poco la vuelta a la tortilla: y si queremos hacer cosas de manera secuencial, siempre tenemos la posibilidad de tener control sobre el estado del programa. Además, el código queda más limpio que si lo quisiéramos hacer sólo con callbacks: más fácil de leer y menos propenso a caer en errores.

Otra ventaja es que siempre nos devuelve algo: tenemos el resultado de la "petición" o la causa por la que no ha llegado. Pero ambas cosas tienen la misma estructura. Incluso, podemos decidir "quién se come ese error" con algo que se llama "Exception error bubbling" (como el bubbling de eventos, a algun@ le sonará...).

Veamos alguna comparativa:

document.querySelector('.button').click(function() {
        buscaUsuario(function (res) {
            servicioExternoAPI.devuelveDatosDeUsuario(res, function(datos) {
                ui.adjuntar(datos);
            });
        });
    });

Comparado con:

document.querySelector('.button').clickQueDevuelvePromesa()
        .then(buscaUsuario) // devuelven promesa
        .then(servicioExternoAPI.devuelveDatosDeUsuario) // devuelven promesa
        .then(ui.adjuntar);
sequential

Las Promesas están hechas para ser encadenadas. Esta manera de usar promesas se llama "sequential join" (la contraposición es "parallel join").

De esta manera también, nos da la posibilidad de manejar los errores en cada paso:

document.querySelector('.button').clickQueDevuelvePromesa()
        .then(buscaUsuario)
        .catch((exception1) => throw new Error(exception1)) // He numerado las "excepciones" para mayor claridad
        .then(servicioExternoAPI.devuelveDatosDeUsuario)
        .catch((exception2) => throw new Error(exception2)) // He numerado las "excepciones" para mayor claridad
        .then(ui.adjuntar);

En vez de simplemente, lanzar un Error, podemos hacer algo en caso de encontrarnos en esa situación. Da una oportunidad para aplicar conceptos del Progressive Enhacement. 😄

Nos da la posibilidad de llevar una sincronía en métodos asíncronos también.

¿Cuándo tenemos que usar promesas?

Para controlar la sequencialidad de código asíncrono y sus respuestas (ya sean valores o excepciones).

Me alegro tanto de que exista una API así..., trabajar con callbacks y CPSs no es nada bonito, en mi opinión...

Los casos más comunes son:

  • peticiones al servidor o a un sitio remoto: y luego tenemos que esperar a usar los datos que devuelve. O queremos saber cuándo ha terminado: por ejemplo: se cargan todas las imágenes de una galería (o si tenemos 404s!).
  • si queremos no bloquear el hilo mientras hacemos operaciones costosas.
  • si necesitamos que un estado esté completado para tratar datos en otra parte de la aplicación. (Aunque tenemos muchas otras técnicas, en frameworks "reactivos" para conseguir esto...)

Casi siempre tiene que ver con llamadas externas.

¿Cómo no hay que usar las promesas?

Es lo que llamamos los Promise Anti-patters.

Será mejor que os pase uno de los tantos enlaces en los que lo explican muy bien: Promise Anti-patterns.
La más chocante de todas, para mí, es: si dentro de un método asíncrono, si hay una promesa, no se devuelve (no se hace un return antes de la promesa), perdemos el control sobre el manejo de errores. Es lo que se llama "romper la cadena".

Broken

Muchas gracias a tod@s los que hayan llegado hasta el final del artículo. Espero que os ayude para introduciros en la programación asíncrona. Y si ya estabais introducid@s, que lo hayáis disfrutado. Espero que, desde que la conocierais ya no podáis vivir sin ella:

Friends

En otros runtimes, como node, tenemos otros sistemas para trabajar con concurrencia. O si quisiéramos aliviar el hilo principal realmente, también podríamos usar Service Workers, ya que de esta manera tenemos un segundo hilo real, donde delegar operaciones pesadas!

Apenas hemos visto algún ejemplo de promesas, pero creo que a modo de introducción, es mejor dejarlo aquí.

Porque la web es un ecosistema vivo y cambiante, tenemos que estar al día de sus nuevas incorporaciones. Tenemos APIs para aburrir y propiedades de CSS nuevas cada poco tiempo –sería el paraíso de una editorial de comics o de una distribuidora de películas: personajes incontables y con agentes añadidos constantemente–. Hoy era el momento de romper una lanza a favor de uno de los pilares de la programación y descubrir las promesas para quien no las conociera.

Flipando

Pero tengo promesas que cumplir,
y andar mucho camino sin dormir,
y andar mucho camino sin dormir.

Robert Frost (1874-1963) Poeta estadounidense


(1) Barbara Liskov; Liuba Shrira (1988). “Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems”. Proceedings of the SIGPLAN ’88 Conference on Programming Language Design and Implementation; Atlanta, Georgia, United States, pp. 260–267. ISBN 0–89791–269–1, published by ACM. Also published in ACM SIGPLAN Notices, Volume 23, Issue 7, July 1988.

Paul S. Melero

Web Dev. I'm passionate about the Web but even more about Culture. Vegetarian. Tweets in English, usually. He/him.