Comprender el bucle de eventos y las devoluciones de llamada

0 acciones
0
0
0
0

Introducción

En los inicios de Internet, los sitios web solían consistir en datos estáticos en una página HTML. Pero ahora que las aplicaciones web se han vuelto más interactivas y dinámicas, se ha vuelto cada vez más necesario realizar operaciones intensivas, como solicitudes de red externas para recuperar datos de la API. Para realizar estas operaciones en JavaScript, un desarrollador debe utilizar técnicas de programación asíncrona.

Dado que JavaScript es un lenguaje de programación de un solo subproceso con un modelo de ejecución concurrente que procesa las operaciones una tras otra, solo puede procesar un comando a la vez. Sin embargo, una acción como solicitar datos de una API puede tardar un tiempo indeterminado, dependiendo del tamaño de los datos solicitados, la velocidad de la conexión de red y otros factores. Si las llamadas a la API se realizaran sincrónicamente, el navegador no podría procesar ninguna entrada del usuario, como desplazarse o hacer clic en un botón, hasta que se complete la operación. Esto se conoce como bloqueo.

Para evitar comportamientos de bloqueo, el entorno del navegador cuenta con numerosas API web asincrónicas a las que JavaScript puede acceder, lo que significa que pueden ejecutarse en paralelo con otras operaciones en lugar de secuencialmente. Esto resulta útil porque permite al usuario seguir usando el navegador con normalidad mientras se procesa la operación asincrónica.

Bucle de eventos

Esta sección explica cómo JavaScript gestiona el código asíncrono con el bucle de eventos. Primero, se muestra una demostración del bucle de eventos en funcionamiento y, a continuación, se explican dos elementos del bucle: la pila y la cola.

El código JavaScript que no utiliza ninguna API web asíncrona se ejecuta sincrónicamente, una a la vez, secuencialmente. Esto se ilustra con este código de ejemplo, que llama a tres funciones, cada una de las cuales imprime un número en la consola:

// Define three example functions
function first() {
console.log(1)
}

function second() {
console.log(2)
}

function third() {
console.log(3)
}

En este código, define tres funciones que imprimen números usando console.log().

Luego, escribe las llamadas a las funciones:

// Execute the functions
first()
second()
third()

La salida se basará en el orden en que se llaman las funciones: primero(), segundo() y luego tres():

Output
1
2
3

Al usar una API web asíncrona, las reglas se complican. Una API integrada con la que puedes experimentar es setTimeout, que establece un temporizador y realiza una acción tras un tiempo especificado. setTimeout debe ser asíncrono; de lo contrario, el navegador se bloqueará mientras esperas, lo que resultará en una mala experiencia de usuario.

Para simular una solicitud asincrónica, agregue setTimeout a la segunda función:

// Define three example functions, but one of them contains asynchronous code
function first() {
console.log(1)
}

function second() {
setTimeout(() => {
console.log(2)
}, 0)
}

function third() {
console.log(3)
}

setTimeout acepta dos argumentos: una función que se ejecuta asincrónicamente y el tiempo de espera antes de llamar a dicha función. En este código, se coloca console.log en una función anónima y se pasa a setTimeout. Luego, se configura la función para que se ejecute después de 0 milisegundos.

Ahora llama a las funciones como antes:

// Execute the functions
first()
second()
third()

Se podría esperar que, al establecer setTimeout en 0, la ejecución de estas tres funciones imprimiera los números en orden secuencial. Sin embargo, al ser asíncrona, la función imprimirá con un salto de línea en el último:

Output
1
3
2

No importa si configura el temporizador a cero segundos o a cinco minutos: console.log, llamado por código asíncrono, se ejecutará después de las funciones síncronas de nivel superior. Esto se debe a que el entorno host de JavaScript (en este caso, el navegador) utiliza un bucle de eventos para gestionar eventos concurrentes o paralelos. Dado que JavaScript solo puede ejecutar un comando a la vez, necesita el bucle de eventos para saber cuándo se ejecuta un comando en particular. El bucle de eventos gestiona esto mediante los conceptos de pilas y colas.

Pila

La pila, o pila de llamadas, contiene el estado de la función que se está ejecutando. Si no está familiarizado con el concepto de pila, puede imaginarla como un array con propiedades LIFO (último en entrar, primero en salir), lo que significa que solo puede agregar o eliminar elementos de la parte inferior de la pila. JavaScript ejecuta el fotograma actual (o la llamada a la función en un entorno específico) en la pila, luego lo elimina y pasa al siguiente fotograma.

Para un ejemplo que contiene solo código sincrónico, el navegador se ejecuta en el siguiente orden:

  • primero() Agregue a la pila, ejecute first() que registra 1 en la consola, elimine first() de la pila.
  • segundo() Agregue a la pila, ejecute second() que imprime 2 en la consola, elimine second() de la pila.
  • tercero () Agregue a la pila, ejecute third() que registra 3 en la consola, elimine third() de la pila.

El segundo ejemplo con setTimout es el siguiente:

  • primero() Agregue a la pila, ejecute first() que registra 1 en la consola, elimine first() de la pila.
  • segundo() Agregue a la pila, ejecute second().
    • Agregue setTimeout() a la pila, ejecute la API web setTimeout() que inicia el temporizador y agrega la función anónima a la cola, elimine setTimeout() de la pila.
  • segundo() Retirar de la pila.
  • Tercero () Agregue a la pila, ejecute third() que registra 3 en la consola, elimine third() de la pila.
  • El bucle de eventos verifica la cola en busca de mensajes pendientes y encuentra la función anónima de setTimeout(), agrega la función a la pila que registra 2 en la consola y luego la elimina de la pila.

Al utilizar setTimeout, una API web asincrónica, se introduce el concepto de cola, que este tutorial cubrirá más adelante.

Cola

Una cola, también llamada cola de mensajes o cola de tareas, es un área de espera para funciones. Cuando la pila de llamadas está vacía, el bucle de eventos revisa la cola en busca de mensajes pendientes, empezando por el más antiguo. Al encontrar uno, lo añade a la pila, que ejecuta la función contenida en el mensaje.

En el ejemplo setTimeout, la función anónima se ejecuta inmediatamente después del resto de la ejecución de nivel superior, ya que el temporizador se configuró en 0 segundos. Es importante recordar que el temporizador no implica que el código se ejecutará exactamente a los 0 segundos ni en un momento específico, sino que añadirá la función anónima a la cola durante ese tiempo. Este sistema de colas existe porque si el temporizador añadiera la función anónima directamente a la pila después de que expire, interrumpiría la función que se está ejecutando, lo que podría tener efectos impredecibles.

Nota: También existe otra cola llamada cola de trabajos o cola de microtareas que gestiona promesas. Las microtareas, como las promesas, se ejecutan con mayor prioridad que las macrotareas como setTimeout.

Ahora que sabes cómo el bucle de eventos usa la pila y la cola para gestionar el orden de ejecución del código, el siguiente paso es descubrir cómo controlar dicho orden. Para ello, primero aprenderás la principal forma de garantizar que el bucle de eventos gestione correctamente el código asíncrono: las funciones de devolución de llamada.

Funciones de devolución de llamada

En el ejemplo setTimeout, la función con el tiempo de espera se ejecuta después de todo en el contexto principal de la ejecución de nivel superior. Sin embargo, si desea asegurarse de que una de las funciones, como la tercera, se ejecute después del tiempo de espera, debe usar técnicas de codificación asíncrona. El tiempo de espera puede representar una llamada API asíncrona que contiene datos. Si desea trabajar con los datos de la llamada API, primero debe asegurarse de que se devuelvan.

La principal solución a este problema es usar funciones de devolución de llamada. Estas funciones no tienen una sintaxis especial. Son simplemente una función que se pasa como argumento a otra función. Una función que toma otra función como argumento se denomina función de orden superior. Según esta definición, cualquier función puede convertirse en una función llamada si se pasa como argumento. Las llamadas telefónicas no son inherentemente asíncronas, pero pueden utilizarse con fines asíncronos.

A continuación se muestra un ejemplo de código de sintaxis de una función de orden superior y una devolución de llamada:

// A function
function fn() {
console.log('Just a function')
}

// A function that takes another function as an argument
function higherOrderFunction(callback) {
// When you call a function that is passed as an argument, it is referred to as a callback
callback()
}

// Passing a function
higherOrderFunction(fn)

En este código, define una función fn, define una función aboveOrderFunction que toma una función de devolución de llamada como argumento y pasa fn como una devolución de llamada a aboveOrderFunction.

Al ejecutar este código se logrará lo siguiente:

Output
Just a function

Volvamos a la primera, segunda y tercera función con setTimeout. Esto es lo que tienes hasta ahora:

function first() {
console.log(1)
}

function second() {
setTimeout(() => {
console.log(2)
}, 0)
}

function third() {
console.log(3)
}

La tarea consiste en que la tercera función retrase siempre la ejecución hasta que se complete la acción asincrónica de la segunda función. Aquí es donde entran en juego las devoluciones de llamada. En lugar de ejecutar la primera, la segunda y la tercera en el nivel superior de ejecución, se pasa la tercera función como argumento a la segunda. Esta última ejecuta la devolución de llamada una vez completada la acción asincrónica.

Aquí hay tres funciones implementadas con una devolución de llamada:

// Define three functions
function first() {
console.log(1)
}

function second(callback) {
setTimeout(() => {
console.log(2)

// Execute the callback function
callback()
}, 0)
}

function third() {
console.log(3)
}

Ahora ejecute el primero y el segundo, luego pase el tercero como argumento al segundo:

first()
second(third)

Después de ejecutar este bloque de código, obtendrá el siguiente resultado:

Output
1
2
3

Primero se imprime 1 y, una vez que expira el temporizador (en este caso es cero segundos, pero puede cambiarlo a cualquier valor), se imprime 2 y luego 3. Trabaje hasta que se complete la API web asincrónica (setTimeout).

El punto clave es que las funciones de devolución de llamada no son asíncronas: setTimeout es una API web asíncrona responsable de gestionar tareas asíncronas. Las devoluciones de llamada solo permiten recibir notificaciones cuando una tarea asíncrona se completa y gestionar su éxito o fracaso.

Ahora que ha aprendido a usar devoluciones de llamadas para realizar tareas asincrónicas, la siguiente sección explica los problemas asociados con la anidación de demasiadas llamadas y la creación de una "pirámide de la perdición".

Devolución de llamada anidada y Pirámide de la perdición

Las funciones de devolución de llamada son una forma eficaz de garantizar que una función se retrase hasta que otra se complete y retorne con datos. Sin embargo, debido a la naturaleza anidada de las devoluciones de llamada, el código puede saturarse si se tienen muchas solicitudes asíncronas consecutivas que dependen unas de otras. Esto suponía una gran frustración para los primeros desarrolladores de JavaScript, y como resultado, el código que contiene llamadas anidadas suele denominarse la "pirámide del tormento" o el "infierno de las devoluciones de llamada".

A continuación se muestra una demostración de devoluciones de llamadas anidadas:

function pyramidOfDoom() {
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
}, 500)
}, 2000)
}, 1000)
}

En este código, cada nuevo setTimeout se coloca dentro de una función de orden superior, creando una pirámide de llamadas cada vez más profundas. Al ejecutar este código, se obtendrá el siguiente resultado:

Output
1
2
3

En la práctica, con código asíncrono real, esto puede complicarse mucho más. Probablemente necesitará gestionar errores en código asíncrono y luego pasar datos de cada respuesta a la siguiente solicitud. Hacer esto con devoluciones de llamada dificulta la lectura y el mantenimiento del código.

He aquí un ejemplo práctico de una “pirámide de la perdición” más realista con la que puedes jugar:

// Example asynchronous function
function asynchronousRequest(args, callback) {
// Throw an error if no arguments are passed
if (!args) {
return callback(new Error('Whoa! Something went wrong.'))
} else {
return setTimeout(
// Just adding in a random number so it seems like the contrived asynchronous function
// returned different data
() => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
500,
)
}
}
// Nested asynchronous requests
function callbackHell() {
asynchronousRequest('First', function first(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest('Second', function second(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest(null, function third(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
})
})
})
}
// Execute 
callbackHell()

En este código, debes tener en cuenta cada función para una posible respuesta y un posible error, lo que hace que la función callbackHell sea visualmente confusa.

Al ejecutar este código obtendrá el siguiente resultado:

Output
First 9
Second 3
Error: Whoa! Something went wrong.
at asynchronousRequest (<anonymous>:4:21)
at second (<anonymous>:29:7)
at <anonymous>:9:13

Resultado

Este método de gestión de código asíncrono es complejo. Por ello, en ES6 se introdujo el concepto de promesas. Este es el tema central de la siguiente sección.

Un comentario
Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

También te puede gustar