Asincronía con callbacks

Gestionar la asincronía con callbacks


Las funciones callback pueden utilizarse como un primer intento de manejar la asincronía en un programa Javascript. Teniendo en cuenta lo que es una función callback, podemos utilizarlas y «anidarlas» para retrasar la ejecución de un cierto código, y de alguna forma, controlar el orden.

Tareas síncronas

Vamos a crear un ejemplo y explicarlo poco a poco, de modo que se entienda lo que estamos haciendo en cada paso. En primer lugar, vamos a crear 3 tareas y colocarlas en una función independiente cada una:

const task1 = () => console.log("Tarea 1 completada");
const task2 = () => console.log("Tarea 2 completada");
const task3 = () => console.log("Tarea 3 completada");

task1();
task2();
task3();

En este caso, se trata de 3 tareas síncronas. Cada función (task1, task2 y task3) son funciones que ejecutan un console.log() anunciando cuando se ha terminado la tarea. En este caso, son tareas síncronas que se ejecutan de forma secuencial y hasta que no termina una, no se inicia la siguiente.

Tareas asíncronas

Sin embargo, vamos a modificar cada función añadiéndole un setTimeout() simplemente para simular que son tareas asíncronas, y que pueden tardar un poco en completarse.

Observa que le he puesto tiempos diferentes a cada una de las tareas:

const task1 = () => setTimeout(() => console.log("Tarea 1 completada en 3 segundos"), 3000);
const task2 = () => setTimeout(() => console.log("Tarea 2 completada en 2 segundos"), 2000);
const task3 = () => setTimeout(() => console.log("Tarea 3 completada en 5 segundos"), 5000);

task1();
task2();
task3();

Ten en cuenta que cada tarea es una función, que ejecuta un setTimeout() al que le pasas una función por parámetro y el tiempo que tardará en ejecutar dicha función. Ahora, a pesar de ejecutarlas en orden, como tardan tiempos diferentes, comprobaremos que terminaría primero la tarea 2 (tarda 2 segundos), luego la tarea 1 (tarda 3 segundos) y finalmente la tarea 3 (tarda 5 segundos).

Es importante tener en cuenta que la forma de funcionar del setTimeout() es no bloqueante, por lo que no se está ejecutando la primera tarea, y bloqueando el resto, sino que se están iniciando las 3 a la vez, y luego irán terminando cada una de ellas.

Asincronía con callbacks

Ahora mismo no garantizan el orden, por lo que vamos a hacer algunos cambios para que respeten el orden original: tarea 1, tarea 2 y tarea 3. Para ello, vamos a convertirlas en funciones callback. Vamos a abandonar el modo de una sola línea para que se entienda mejor:

const task1 = (callback) => {
  console.log("Iniciando tarea 1...");
  setTimeout(() => {
    callback();
  }, 3000);
}

task1( () => console.log("Tarea 1 terminada en 3 segundos") );

Expliquemos las modificaciones realizadas:

  • 1️⃣ Ahora la función task1() tiene un parámetro callback (ver último punto)
  • 2️⃣ Hemos puesto un console.log() fuera del setTimeout() (se ejecutará inmediatamente)
  • 3️⃣ Estamos ejecutando callback() dentro del setTimeout() (se ejecutará después de los 3 segundos)
  • 4️⃣ Observa la última línea: ejecutamos task1() y le pasamos la función que será callback

Esto hará que nos aparezca:

Iniciando tarea 1...
(3 segundos después)
Tarea 1 terminada en 3 segundos

¡Perfecto! Primer paso hecho. Ya estamos controlando una tarea. Ahora vamos a controlar las 3 tareas. Para ello, aplicamos lo mismo a las 3 tareas, cada una con su tiempo correspondiente. Pero observa cuando ejecutamos las tareas: ahora cada tarea se ejecuta dentro de una función callback, de latarea anterior.

De esta forma garantizamos el orden de ejecución:

const task1 = (callback) => {
  console.log("Iniciando tarea 1...");
  setTimeout(() => {
    callback();
  }, 3000);
}

const task2 = (callback) => {
  console.log("Iniciando tarea 2...");
  setTimeout(() => {
    callback();
  }, 2000);
}

const task3 = (callback) => {
  console.log("Iniciando tarea 3...");
  setTimeout(() => {
    callback();
  }, 5000);
}

task1( () => {
  console.log("Tarea 1 completada.");
  task2( () => {
    console.log("Tarea 2 completada.");
    task3( () => {
      console.log("Tarea 3 completada.");
    });
  });
});
const task = (name, time, callback) => {
  console.log(`Iniciando ${name}...`);
  setTimeout(() => {
    callback();
  }, time);
}

task( "tarea 1", 3000, () => {
  console.log("Tarea 1 completada.");
  task( "tarea 2", 2000, () => {
    console.log("Tarea 2 completada.");
    task( "tarea 3", 5000, () => {
      console.log("Tarea 3 completada.");
    });
  });
});

Observa que en este fragmento de código tienes dos pestañas. En la segunda tienes el mismo ejemplo, pero reutilizando una misma tarea, pasándole el nombre y el tiempo por parámetro, por si prefieres esa versión. Pero ambas son equivalentes.

Este patrón era muy común en Javascript, y era muy utilizado en la época dorada de jQuery, donde muchas funciones o librerías se utilizaban con funciones callback como argumentos.

Desventajas: Callback Hell

A pesar de ser una forma flexible y potente de controlar la asincronía, con muchas posibilidades, las funciones callbacks tienen ciertas desventajas evidentes. En primer lugar, el código creado con las funciones es algo caótico y (quizás subjetivamente) bastante feo. Por ejemplo, el tener que pasar un por parámetros en algunas funciones, no es demasiado elegante.

Pero sobre todo, uno de los problemas evidentes viene a la hora de tener que gestionar la asincronía varias veces en una misma función, donde al introducir varias funciones con callbacks en su interior, conseguimos una estructura anidada similar a la siguiente:

Callback Hell

La forma triangular que produce es conocida como Callback Hell o Pyramid of Doom, debido a su forma, resultando un código muy poco elegante que se puede complicar demasiado de cara a la legibilidad.

Para solucionar estos problemas, entran en juego las promesas, un sistema de control de asincronía que simplifica mucho el código y evita las desventajas de las funciones callback.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev