Programación Asíncrona en JavaScript y Callbacks
Asincronía en Lenguajes de Programación
Las computadoras son asíncronas por diseño.
Asíncrono significa que las cosas pueden suceder independientemente del flujo principal del programa.
En las computadoras de consumo actuales, cada programa se ejecuta durante un intervalo de tiempo específico y luego detiene su ejecución para permitir que otro programa continúe su ejecución. Esto sucede en un ciclo tan rápido que es imposible de notar. Creemos que nuestras computadoras ejecutan muchos programas simultáneamente, pero esto es una ilusión (excepto en máquinas multiprocesador).
Los programas usan internamente interrupciones, una señal que se emite al procesador para captar la atención del sistema.
No vamos a entrar en los detalles internos de esto ahora, pero solo tenga en cuenta que es normal que los programas sean asíncronos y detengan su ejecución hasta que necesiten atención, permitiendo que la computadora ejecute otras cosas mientras tanto. Cuando un programa está esperando una respuesta de la red, no puede detener el procesador hasta que la solicitud finalice.
Normalmente, los lenguajes de programación son sincrónicos y algunos proporcionan una manera de gestionar la asincronía en el lenguaje o mediante bibliotecas. C, Java, C#, PHP, Go, Ruby, Swift y Python son todos sincrónicos por defecto. Algunos de ellos manejan operaciones asíncronas utilizando threads, generando un nuevo proceso.
JavaScript
JavaScript es sincrónico por defecto y es monohilo. Esto significa que el código no puede crear nuevos threads y ejecutarse en paralelo.
Las líneas de código se ejecutan en serie, una tras otra, por ejemplo:
const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();
Pero JavaScript nació dentro del navegador, su trabajo principal, al principio, era responder a las acciones del usuario, como onClick
, onMouseOver
, onChange
, onSubmit
y así sucesivamente. ¿Cómo podría hacerlo con un modelo de programación sincrónica?
La respuesta estaba en su entorno. El navegador proporciona una manera de hacerlo proporcionando un conjunto de APIs que pueden manejar este tipo de funcionalidad.
Más recientemente, Node.js introdujo un entorno de E/S no bloqueante para extender este concepto al acceso a archivos, llamadas de red, etc.
Devoluciones de llamada (Callbacks)
No se puede saber cuándo un usuario va a hacer clic en un botón. Por lo tanto, se define un controlador de eventos para el evento de clic. Este controlador de eventos acepta una función, que se llamará cuando se active el evento:
document.getElementById('button').addEventListener('click', () => {
// elemento pulsado
});
Esta es la llamada devolución de llamada (callback).
Una devolución de llamada es una función simple que se pasa como valor a otra función, y solo se ejecutará cuando ocurra el evento. Podemos hacer esto porque JavaScript tiene funciones de primera clase, que se pueden asignar a variables y pasar a otras funciones (llamadas funciones de orden superior).
Es común envolver todo el código del cliente en un escuchador de eventos load en el objeto window, que ejecuta la función de devolución de llamada solo cuando la página está lista:
window.addEventListener('load', () => {
// ventana cargada
// haz lo que quieras
});
Las devoluciones de llamada se utilizan en todas partes, no solo en los eventos DOM.
Un ejemplo común es el uso de temporizadores:
setTimeout(() => {
// se ejecuta después de 2 segundos
}, 2000);
Las solicitudes XHR también aceptan una devolución de llamada, en este ejemplo asignando una función a una propiedad que se llamará cuando ocurra un evento particular (en este caso, cambia el estado de la solicitud):
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
xhr.status === 200 ? console.log(xhr.responseText) : console.error('error');
}
};
xhr.open('GET', 'https://yoursite.com');
xhr.send();
Manejo de errores en las devoluciones de llamada
¿Cómo se manejan los errores con las devoluciones de llamada? Una estrategia muy común es usar lo que Node.js adoptó: el primer parámetro en cualquier función de devolución de llamada es el objeto de error: devoluciones de llamada con error primero (error-first callbacks)
Si no hay ningún error, el objeto es nulo. Si hay un error, contiene alguna descripción del error y otra información.
const fs = require('node:fs');
fs.readFile('/file.json', (err, data) => {
if (err) {
// manejar error
console.log(err);
return;
}
// sin errores, procesar datos
console.log(data);
});
El problema con los callbacks
Los callbacks son geniales para casos simples.
Sin embargo, cada callback añade un nivel de anidamiento, y cuando tienes muchos callbacks, el código empieza a complicarse muy rápidamente:
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
// your code here
});
}, 2000);
});
});
Este es solo un código simple de 4 niveles, pero he visto muchos más niveles de anidamiento y no es divertido.
¿Cómo lo resolvemos?
Alternativas a los callbacks
A partir de ES6, JavaScript introdujo varias características que nos ayudan con el código asíncrono que no implica el uso de callbacks: Promises
(ES6) y Async/Await
(ES2017).