martes, 7 de marzo de 2017

Programación asíncrona con Javascript - Parte 1, lo básico (event queue, call stack, event loop, timers y DOM events)

Hola de nuevo, ya que últimamente he estado escribiendo bastante sobre javascript es necesario, para comprender mejor lo que se hace, conocer que es la programación asíncrona (pista: la programación asincrona permite realizar operaciones en segundo plano :D).

Pero primero, ¿porqué es necesaria la programación asíncrona?, el problema es que javascript es single threaded (se ejecuta sobre un solo hilo), lo que imposibilita realizar más de una tarea a la vez. Dado que las tareas se ejecutan una tras otra y que se tiene un entorno single threaded, el hecho de que una tome demasiado tiempo para realizarse evita que las demas tareas se ejecuten y es por esto que los procesos que comunmente tardan (I/O) se ejecutan asíncronamente, para evitar que se tenga que bloquear la realización de otras tareas.

Un ejemplo de un proceso que toma demasiado tiempo en ejecutarse y bloqueara la ejecución del programa hasta que finalice:

function tareaCorta(param) {
  console.log('Esta ejecutandosé la tarea corta ' + param);
}

function tareaLarga(param) {
  console.log('Esta ejecutandosé la tarea larga ' + param);
  for(var i = 0; i <= 5000000000; i++) {
    // bucle que tarda demasiado en ejecutarse
  }
  console.log('Terminó la tarea larga ' + param);
}

tareaCorta(1);
tareaCorta(2);
tareaLarga(3);
tareaCorta(4);
tareaCorta(5);

Y como se puede observar, el programa se detiene para ejecutar la tarea larga (que es simplemente un bucle muuuuuy largo):
Ejemplo de la ejecución del código
No siempre es sencillo comprender el concepto de asincronía, así que diagramaremos el comportamiento en el tiempo del siguiente fragmento de pseudo-código.

TAREA1();
RESULTADO_TAREA_2 = TAREA2();
TAREA3(RESULTADO_TAREA_2);
TAREA4();

Mensajes asíncronos y síncronos
Como se puede observar en la línea de tiempo del gráfico (cada flecha indica la existencia de un hilo), en el estilo síncrono cada tarea debe esperar a que la anterior concluya para realizar otra operación, sin embargo, introduciendo la asincronía si se tiene una operación que consume demasiado tiempo (comunmente I/O), la tarea se ejecuta de manera asíncrona (en otro entorno de ejecución), y luego se puede completar el proceso (usualmente al terminar la tarea asincrona se agrega el callback respectivo al event queue con los resultados de la operación asíncrona). En la imagen se puede observar que dado que la tarea4 no depende de la ejecución de la tarea2, esta se lanza inmediatamente aunque la tarea anterior no haya finalizado.

Para conceptualizar mejor la idea, es necesario estudiar el siguiente gráfico del mismo pseudocógido anterior:
Ejecución asíncrona vs síncrona
Esta vez el tiempo de espera de la petición por la red se ha reducido, y si bien en el primer caso (síncrono) la ejecución no varia demasiado, se puede observar que en el caso de la ejecución asíncrona, aunque la petición por la red ya ha terminado, la tarea3 no puede ejecutarse hasta que la tarea4 haya concluido porque nuestro entorno de ejecución es single threaded. La idea se resume en: todo corre en un hilo distinto excepto nuestro código.

Ahora, algunos conceptos que son de importancia:
  • call stack (pila de llamadas): mantiene un listado de las funciones que están ejecutandose. Si la funcion1 llama a la funcion2, entonces en el callstack está ejecutandose la funcion2 y está en espera en el mismo call stack la funcion1.
  • event queue (cola de eventos): es una cola que mantiene mensajes que referencian a las funciones que estan en espera de ser puestas en el call stack para ser ejecutadas. Las tareas son agregadas a la cola de eventos por web apis que corren en paralelo con el entorno de javascript.
    • web api: agregan tareas al event queue, por ejemplo:
      • timers: programan tareas para ser agregadas al event queue en un determinado tiempo.
      • DOM event handlers: agrega interacciones del usuario, como eventos del mouse o del teclado al event queue.
      • network requests: las peticiones por la red son procesadas asincronamente y devuelven resultados agregando tareas al event queue.
  • event loop (ciclo de eventos): cuando el call stack esta vacio, toma la primera tarea del event queue y la procesa. Las tareas que quedan en la cola (queue) esperan hasta que el call stack esta limpio nuevamente. Este ciclo se llama event loop.

Ejemplo del event queue y el call stack:

function func1(){
  console.log('tarea1');
}

function func2(){
  console.log('tarea1');
}

func1();
func2();

Ejemplo del call stack y del event queue
Ese fue un ejemplo bastante simple, como se puede observar las tareas van ejecutandosé una a una en el call stack hasta que este queda vacio nuevamente. Pero, ¿qué pasaría si las tareas estuvieran relacionadas?

function func1(){
  func2();
}

function func2(){
  console.log('Tarea completadas');
}

func1();

Ejemplo del event queue y call stack
En este último caso podemos ver como se comportan lo seventos cuando estan relacionados, se van agregando al call stack todas las tareas necesarias para que termine la primera tarea que se insertó. En el transcurso del tiempo en que tardan en ejecutarse estas tareas relacionadas, otros eventos pudieron agregar tareas al call stack.

Es posible que las apis web agreguen los resultados de sus procesos como tareas a la cola de eventos (event queue). Estas tareas son definidas por funciones callbacks que se pasan a las apis web.

- callback functions: Son funciones que se pasan como argumentos para ser ejecutadas en un punto futuro del tiempo.

- anonymous callbacks: Son funciones creadas sin nombre. Son útiles cuando el callback solamente necesita ser ejecutado una vez.

Como ejemplo, veamos una función callback con nombre y una función callback anónima:

var numeros = [1, 2, 3, 4, 5];
console.log(numeros);

// callback anónimo, multiplica por dos
var numerosDuplicados = numeros.map(function(valor) {
  return valor * 2;
});
console.log(numerosDuplicados);

function multiplicaPorTres (valor) {
  return valor * 3;
}
// callback con una función que tiene un nombre
var numerosTriplicados= numeros.map(multiplicaPorTres);
console.log(numerosTriplicados);

Timers
Son funciones nativas de javascript que permiten retrasar la ejecución de instrucciones de la manera que necesitemos. El tiempo que necesitamos que se retrasen se calcula asíncronamente.
  • setInterval() se utiliza para programar una tarea recurrente en el tiempo
  • clearInterval() se utiliza para detener una tarea que se creó con setInterval()
  • setTimeout() se  utiliza para programar una tarea en un determinado tiempo
Ahora como ejemplo ejecutaremos la misma tarea del primer ejemplo, pero ahora la primera tarea se retrase 1000 milisegundos (1 segundo).

function tareaCorta(param) {
  console.log('Esta ejecutandosé la tarea corta ' + param);
}

function tareaLarga(param) {
  console.log('Esta ejecutandosé la tarea larga ' + param);
  for(var i = 0; i <= 5000000000; i++) {
    // bucle que tarda demasiado en ejecutarse
  }
  console.log('Terminó la tarea larga ' + param);
}

setTimeout(function(){
  tareaCorta(1)
}, 5000);
tareaCorta(2)
tareaLarga(3);
tareaCorta(4);
tareaCorta(5);

El resultado de la ejecución es el siguiente:

Ejecución con timeout
Como se puede observar, si bien se programó la tarea para que se ejecute en 5 segundos.
El timer funciona correctamente, la tarea se ejecuta cinco segundos despues. Ahora, veamos otro ejemplo un poco mas extraño:

function tareaCorta(param) {
  console.log('Esta ejecutandosé la tarea corta ' + param);
}

function tareaLarga(param) {
  console.log('Esta ejecutandosé la tarea larga ' + param);
  for(var i = 0; i <= 5000000000; i++) {
    // bucle que tarda demasiado en ejecutarse
  }
  console.log('Terminó la tarea larga ' + param);
}

setTimeout(function(){
  tareaCorta(1)
}, 0);
tareaCorta(2)
tareaLarga(3);
tareaCorta(4);
tareaCorta(5);

Como podemos observar, ahora establecimos el timeout a 0, o sea que la tarea se programa para ejecutarse de inmediato. Veamos el resultado:
Resultado de la ejecución
Lo interesante del ejemplo es que la tareaCorta(1) aún se ejecuta al final, pero ¿porqué, si su timeout (tiempo de espera) es cero?, el problema es el orden en el que se agregan las tareas al callstack, veamos (Revisar el diagrama de Ejecución asíncrona vs síncrona que se agrego en uno de los puntos anteriores):

  • Se "programa" la ejecución del timeout en 0 segundos (la tarea ya esta lista para agregarse, pero estamos esperando al siguiente ciclo de eventos)
  • Se agrega al call stack la tareaCorta(2) para su ejecución
  • Se agrega al call stack la tareaLarga(3) para su ejecución
  • Se agrega al call stack la tareaCorta(4) para su ejecución
  • Se agrega al call stack la tareaCorta(5) para su ejecución (ya no existen más elementos para ejecutarse en el call stack así que ahora se agrega la tarea que se programó antes)
  • Se agrega al call stack la tareaCorta(1) para su ejecución

Se programa el timeout, se agregan al call stack las cuatro tareas y luego se agrega la tarea que esta esperando a ser ejecutada por el timeout con 9 milisegundos de espera.

DOM event listeners

Los "oyentes" de eventos del DOM ocurren paralelamente al cógido javascript ejecutado (asíncronamente). Los event listeners detectan un evento y lo agregan al event queue para que el evento sea ejecutado en el futuro. En caso de tener muchos eventos que suceden uno tras otro, todos se agregan en el orden en que ocurren a la cola de eventos (event queue). Algunos eventos que pueden suceder son: keypressed, keyup, mouseenter, mouseleave, etc.

Ahora un ejemplo de el manejo de eventos con javascript super sencillo (lo importante es recordar que los listeners siempre estan escuchando y agregarán el evento al event queue en cualquier momento). Crear en una carpeta el archivo example.html y example.js. El código del archivo example.html es:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Eventos</title>
</head>
<body>
    <div id="div-rojo" style="background-color: red;height:200px;"></div>
    <br />
    <div id="elemento"></div>
    <script src="example.js"></script>
</body>
</html>

Simplemente creamos dos divs, y agregamos el javscript con el nombre "example.js". Ahor el código javascript:

document.getElementById('div-rojo')
  .addEventListener('mouseenter', function(){
    document.getElementById('elemento')
            .innerHTML = 'El mouse ha entrado al div rojo';
});

document.getElementById('div-rojo')
  .addEventListener('mouseleave', function(){
    document.getElementById('elemento')
            .innerHTML = 'El mouse ha salido del div rojo';
});

Con esto, obtenemos el primer div (div-rojo), y le agregamos un listener para el evento mouseenter, cuando el evento suceda, obtenemos el otro div y escribimos el mensaje El mouse ha entrado al div rojo. También, obtenemos el primer div (div-rojo) y le agregamos el evento contrario (mouseleave), pero para que se note el cambio modificamos el mensaje del otro div diciendo que El mouse ha salido del div rojo.

Ejemplo de ejecución (no tomen en cuenta las manchas, es para mantener bajo el tamaño del gif XD)
Y eso es todo por ahora, keep coding!!! :)

No hay comentarios:

Publicar un comentario