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ámetrocallback
(ver último punto) - 2️⃣ Hemos puesto un
console.log()
fuera delsetTimeout()
(se ejecutará inmediatamente) - 3️⃣ Estamos ejecutando
callback()
dentro delsetTimeout()
(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
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:
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.