En el mundo de la programación nos encontramos distintas formas de solucionar un problema, a menudo agrupadas en lo que se conoce como paradigmas. ¿Paradi-qué?

Un paradigma no es más que el enfoque que se aplica a la hora de diseñar la solución a un problema determinado, la base en la que construimos una lógica que nos ayude a entender como lograr una tarea.

En esta serie de capítulos sobre Javascript ya hemos visto y explicado tres paradigmas distintos: programación imperativa, programación orientada a objetos y programación funcional. Nos queda por tanto explicar el último paradigma que está en estrecha relación y define tanto a Javacript: la programación asíncrona.

Comúnmente, la manera más extendida y con la que normalmente se aprende a programar es la programación imperativa. Se nos da una serie de herramientas o instrucciones que un ordenador interpretará de manera secuencial para llegar a una resolución final. Por tanto, el orden de dichas instrucciones determinará el resultado, y cada una de ellas será ejecutada una vez la anterior haya finalizado. Veamos un ejemplo.

function range(min, max) {  
  for (var i=min; i<max; i++) {
    console.log(i)
  }
}

range(0, 3);  
console.log('end');

// 0, 1, 2, 'end'

La función range ejecutará un bucle que bloqueará el resto del programa hasta que finalice su ejecución. ¿Que pasaría si en lugar de 3 le pasáramos un número enorme? 'end' tardaría mucho más tiempo en aparecer. Modifiquemos un poco el código anterior.

function range(min, max) {  
  setTimeout(() => {
    for (var i=min; i<max; i++) {
      console.log(i)
    }
  }, 100); // ms
}

range(0, 3);  
console.log('end');

// 'end', 0, 1, 2

Aunque el código hace lo mismo, no se ejecuta en el mismo orden. 'end' aparece antes que los números. ¿Por qué? Porque hemos ejecutado el bucle dentro de un contexto asíncrono. Claro, porque le has añadido un retraso a la función de 100ms... Ponlo a 0 y verás.

function range(min, max) {  
  setTimeout(() => {
    for (var i=min; i<max; i++) {
      console.log(i)
    }
  }, 0); // ms
}

range(0, 3);  
console.log('end');

// 'end', 0, 1, 2

En este caso el 0 es irrelevante. Aunque le digamos a setTimeout que se ejecute inmediatamente, la función que le pasemos parámetro se ejecuta de forma asíncrona y no bloquea el resto de la ejecución del programa.

Esto se consigue gracias a una cola de ejecución. Las instrucciones que se ejecutan de forma asíncrona no lo hacen de forma secuencial con el resto del programa, sino que se les da una prioridad menor y se añaden a una cola de ejecución en la que quedan a la espera de ser procesadas.

Callbacks

En Javascript casi todo se consigue mediante funciones. Para conseguir ejecutar procesos de forma asíncrona pasamos funciones como parámetro a otras funciones que el lenguaje ya establece como asíncronas. Estas funciones que usamos como parámetro reciben el nombre de callbacks.

En el ejemplo anterior la callback es la arrow function que le hemos pasado como parámetro a setTimeout. Dicha función se define de forma anónima pero podríamos definirla fuera y usarla las veces que queramos.

function loop() {  
  for (var i=min; i<max; i++) {
    console.log(i)
  }
}

setTimeout(loop, 3000); // 0, 1, 2

console.log('Loop will start in 3s...')  

Las callbacks nos resuelven el problema de lidiar con procesos largos o de espera sin que tengamos que bloquear el hilo de ejecución y por tanto el resto del programa.

De momento los ejemplos que he puesto se limitan a posponer la ejecución de un bucle. Pero ¿que pasaría si tuviéramos que calcular un resultado y asignárselo a otra variable? Podemos hacer que la callback modifique una variable de nuestro contexto global.

var result;

setTimeout(() => {  
  result = 1;
}, 3000);

console.log(result) // undefined  

Sin embargo al ser asíncrono no podemos determinar cuando podremos leer result. ¿Cómo podríamos saber en que momento result se modificará para poder leerla? Necesitaríamos que la función nos avisara de que ha acabado.

Eventos

La implementación mas común de programación asíncrona es la programación dirigida por eventos. Un evento es el cambio de estado en un sistema, proceso o entidad, bien porque se ha ejecutado una instrucción o porque se ha producido una acción externa.

El caso de eventos mas común es el de una interfaz de usuario. Normalmente las interfaces se diseñan de forma asíncrona para no bloquear el resto del proceso a la espera de acciones por parte del usuario, y para que cuando se produzcan éstas el usuario no tenga que esperar a que el programa finalice para que se ejecuten. Veamos un ejemplo.

Nota: Esta vez en lugar de Node usaremos la consola de un navegador web ya que vamos a definir un botón de forma visual en el DOM. Podéis usar Chrome ó Firefox que ya soportan arrow functions y abrir cualquier página.

Queremos tener un botón que imprima un mensaje cada vez que hagamos click en él.

var button = document.createElement('button');  
button.innerHTML = 'Hello';  
button.addEventListener('click', () => console.log('World'));  
document.body.appendChild(button);  

Podéis comprobar el resultado haciendo click sobre el nuevo botón.

Lo importante aquí es la función addEventListener. Todos los elementos HTML del DOM tienen este método que se encarga de registrar el evento que le digamos y ejecutar una función cuando se produzca.

En este caso el evento es click que se produce, como su nombre indica, hacer click sobre el botón que le hemos dicho.

Si volvemos al ejemplo de antes, podríamos usar eventos para saber cuando un proceso ha acabado y conocer su resultado.

Nota: En esta ocasión vamos a simular un evento de forma simple. Más adelante explicaré el patrón observer y veremos la implementación nativa de eventos en Node.

var result;  
var events = {};

setTimeout(() => {  
  result = 2;
  if (events.finish) events.finish();
}, 3000);

events.finish = () => console.log(result);

// 3s ... '2'

La función dentro de setTimeout comprueba si existe el evento finish y en ese caso ejecuta la callback que le hemos definido. Podríamos tener mas eventos que nos fueran avisando de otras acciones como por ejemplo de cuando empezara el proceso.

var result;  
var events = {};

setTimeout(() => {  
  if (events.started) events.started();
  setTimeout(() => {
    result = 2;
    if (events.finish) events.finish();
  }, 1000);
}, 3000);

events.started = () => console.log('Process started!');  
events.finish = () => console.log(result);

// 3s ...
// 'Process started!'
// 1s ...
// '2'

Otro ejemplo común de eventos y callbacks es el de realizar peticiones HTTP. La mayoría de páginas web hacen peticiones HTTP para consultar información sin tener que recargar de nuevo la página. Sin embargo una petición HTTP puede tardar tiempo si nuestra conexión es lenta o si tenemos que descargar muchos datos, es por ello que lo hacen de forma asíncrona registrando eventos.

En este caso voy a comentar un ejemplo extraído de la página de Mozilla.

var req = new XMLHttpRequest(); // (1)  
req.open('GET', 'http://www.mozilla.org/', true); // (2)  
req.onreadystatechange = function (aEvt) { // (4)  
  if (req.readyState == 4) {
     if(req.status == 200)
      dump(req.responseText);
     else
      dump("Error loading page\n");
  }
};
req.send(); // (3)  

Nota: El orden está dispuesto en el orden de ejecución.

  1. Creamos un nuevo objeto de tipo XMLHttpRequest que nos provee la API de HTML5 para realizar una petición HTTP.
  2. Le decimos a nuestro objeto que queremos pedir la página http://www.mozilla.org/ de forma asíncrona (el parámetro true).
  3. req.onreadystatechange es la callback que se ejecutará cuando se dispare el evento readystatechange que será cuando la petición HTTP haya acabado. Procesamos por tanto el resultado en dicha callback.
  4. Enviamos la petición.

Aunque invirtiéramos el código, y pusiéramos req.send (3) antes definir la callback (4), el resultado sería el mismo ya que req.send es asíncrono. A su vez, req.send se encargará de generar el evento readystatechange una vez haya acabado la petición. Es entonces cuando comprobaremos que nos ha devuelto el servidor y lo procesaremos con nuestra callback (4).

Conclusión

La programación asíncrona nos permite conseguir una mayor eficiencia en nuestro software ya que no bloqueamos nuestro hilo de ejecución para procesos largos o interacciones con el usuario. Sin embargo, para muchos resulta compleja de entender en un principio por estar acostumbrados a pensar de forma imperativa y secuencial.

Tanto para el desarrollo de aplicaciones para Node como para navegadores requiere que cambiemos un poco esa forma de pensar, por lo que recomiendo familiarizarse con los conceptos y leer documentación o ejemplos sobre ello.

A partir de aquí, en esta serie de capítulos vamos a hacer un mayor uso de callbacks y eventos. Pero también quiero introducir en breve una nueva serie de artículos sobre patrones de diseño donde comentaré algunos orientados a eventos. Stay tuned.

"Siempre he pensado que la gracia de leer un libro reside en no conocer el final" - Memento (2000)