preloader

Introducción a los callbacks en JavaScript

Introducción a los callbacks en JavaScript

Para ilustrar el uso de callbacks, promesas y otros conceptos abstractos, utilizaremos algunos métodos del navegador, en particular aquellos relacionados con la carga de scripts y la manipulación básica del DOM.

Si no estás familiarizado con estos métodos y los ejemplos te resultan confusos, te recomiendo leer algunos capítulos de esta sección del tutorial.

De todas formas, intentaremos aclarar los conceptos. No habrá nada realmente complicado en términos de navegación.

JavaScript proporciona numerosas funciones que permiten programar acciones asíncronas, es decir, acciones que iniciamos ahora pero que se completan más tarde.

Por ejemplo, una de esas funciones es setTimeout.

Otros ejemplos de acciones asíncronas incluyen la carga de scripts y módulos (que se cubrirán en capítulos posteriores).

Considera la función loadScript(src), que carga un script desde una ruta específica:

				
					function loadScript(src) {
  // crea una etiqueta <script> y la agrega a la página
  // esto hace que el script en src comience a cargarse y se ejecute cuando la carga se complete
  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}

				
			

Esta función inserta dinámicamente una nueva etiqueta <script src="..."> en el documento. El navegador comienza a cargar el script automáticamente y lo ejecuta una vez que se ha cargado completamente.

Podemos usar esta función de la siguiente manera:

				
					// cargar y ejecutar el script en la ruta dada
loadScript('/my/script.js');

				
			

El script se carga de manera «asíncrona», lo que significa que comienza a cargarse ahora, pero se ejecuta más tarde, cuando la función ya ha terminado.

El código debajo de loadScript(...) no espera a que se complete la carga del script.

				
					loadScript('/my/script.js');
// el código debajo de loadScript
// no espera a que se complete la carga del script
// ...

				
			

Supongamos que necesitamos usar el nuevo script tan pronto como se cargue. Este script declara nuevas funciones, y queremos ejecutarlas.

Si intentamos hacerlo inmediatamente después de llamar a loadScript(...), no funcionará:

				
					loadScript('/my/script.js'); // el script tiene a "function newFunction() {…}"

newFunction(); // ¡No existe tal función!

				
			

Esto es natural, porque el navegador no ha tenido tiempo de cargar el script. Hasta el momento, la función loadScript no proporciona una manera de monitorear cuándo se completa la carga. El script se carga y eventualmente se ejecuta, eso es todo. Pero necesitamos saber cuándo sucede para poder usar las nuevas funciones y variables del script.

Agreguemos un segundo argumento a loadScript: una función callback que se ejecuta cuando se completa la carga del script:

				
					function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(script);

  document.head.append(script);
}

				
			

El evento onload, que se describe en el artículo sobre carga de recursos (onload y onerror), básicamente ejecuta una función después de que el script ha sido cargado y ejecutado.

Ahora, si queremos llamar a las nuevas funciones del script, lo hacemos dentro de la callback:

				
					loadScript('/my/script.js', function() {
  // la callback se ejecuta después de que el script se haya cargado
  newFunction(); // ahora funciona
  ...
});

				
			

Esa es la idea: el segundo argumento es una función (generalmente anónima) que se ejecuta cuando la acción se completa.

Aquí hay un ejemplo ejecutable con un script real:

				
					function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  script.onload = () => callback(script);
  document.head.append(script);
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
  alert(`Genial, el script ${script.src} está cargado`);
  alert(_); // _ es una función declarada en el script cargado
});

				
			

Esto se llama programación asincrónica basada en callbacks. Una función que realiza una acción de forma asincrónica debería aceptar un argumento de callback donde se coloca la función que se ejecutará después de que la acción se complete.

Aquí lo hicimos en loadScript, pero, por supuesto, es un enfoque general.

Callback dentro de una callback

¿Cómo podemos cargar dos scripts secuencialmente, el segundo tan pronto como termine de cargarse el primero?

La solución natural sería poner la segunda llamada loadScript dentro de la callback de la primera, así:

				
					loadScript('/my/script.js', function(script) {
  alert(`Genial, el ${script.src} está cargado, carguemos uno más`);

  loadScript('/my/script2.js', function(script) {
    alert(`Genial, el segundo script está cargado`);
  });
});

				
			

Una vez que se completa el loadScript externo, la callback inicia el interno.

¿Qué pasa si queremos cargar un script más?

				
					loadScript('/my/script.js', function(script) {
  loadScript('/my/script2.js', function(script) {
    loadScript('/my/script3.js', function(script) {
      // ...continúa después de que se han cargado todos los scripts
    });
  });
});

				
			

Entonces, cada nueva acción está dentro de una callback. Esto es adecuado para algunas acciones, pero no en todos los casos; por lo que pronto veremos otras variantes.

Manejo de errores

En los ejemplos anteriores no consideramos los errores. ¿Qué pasa si falla la carga del script? Nuestra callback debería poder reaccionar ante eso.

Aquí hay una versión mejorada de loadScript que monitorea los errores de carga:

				
					function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Error al cargar el script ${src}`));

  document.head.append(script);
}

				
			

Para una carga exitosa, llama a callback(null, script) y, de lo contrario, a callback(error).

				
					loadScript('/my/script.js', function(error, script) {
  if (error) {
    // maneja el error
  } else {
    // script cargado satisfactoriamente
  }
});

				
			

Una vez más, la receta que usamos para loadScript es bastante común. Es un estilo conocido como «callback con el error primero».

La convención es:

  • El primer argumento de la callback está reservado para un error, si ocurre. En tal caso, se llama a callback(err).
  • El segundo argumento (y los siguientes si es necesario) son para el resultado exitoso. En este caso, se llama a callback(null, result1, result2, ...).

De esta manera, usamos una única función de callback tanto para informar errores como para transferir resultados.

Pirámide infernal

A primera vista, esta parece una forma viable de codificación asincrónica. Y, de hecho, lo es. Para una o quizás dos llamadas anidadas, se ve bien.

Pero para múltiples acciones asincrónicas que se ejecutan una tras otra, tendremos un código como este:

				
					loadScript('1.js', function(error, script) {
  if (error) {
    handleError(error);
  } else {
    loadScript('2.js', function(error, script) {
      if (error) {
        handleError(error);
      } else {
        loadScript('3.js', function(error, script) {
          if (error) {
            handleError(error);
          } else {
            // ...continúa después de que se han cargado todos los scripts (*)
          }
        });
      }
    });
  }
});

				
			

En el código anterior:

  1. Cargamos 1.js, luego, si no hay error…
  2. Cargamos 2.js, luego, si no hay error…
  3. Cargamos 3.js, luego, si no hay error: hacemos otra cosa (*).

A medida que las llamadas se anidan más, el código se vuelve más profundo y difícil de administrar, especialmente si tenemos un código real en lugar de ‘…’ que puede incluir más bucles, declaraciones condicionales, etc.

A esto se le llama «infierno de callbacks» o «pirámide infernal».

La «pirámide» de llamadas anidadas crece hacia la derecha con cada acción asincrónica. Pronto se sale de control.

Esta forma de codificación no es óptima.

Podemos intentar aliviar el problema haciendo, para cada acción, una función independiente:

				
					loadScript('1.js', paso1);

function paso1(error, script) {
  if (error) {
    handleError(error);
  } else {
    loadScript('2.js', paso2);
  }
}

function paso2(error, script) {
  if (error) {
    handleError(error);
  } else {
    loadScript('3.js', paso3);
  }
}

function paso3(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...continúa después de que se han cargado todos los scripts (*)
  }
}

				
			

¿Lo ves? Hace lo mismo, y ahora no hay anidamiento profundo porque convertimos cada acción en una función de nivel superior separada.

Funciona, pero el código parece un listado desgarrado. Es difícil de leer, y habrás notado que hay que saltar de un lado a otro mientras lees. Es un inconveniente, especialmente si el lector no está familiarizado con el código y no sabe dónde dirigir la mirada.

Además, las funciones llamadas paso* son de un solo uso, existen únicamente para evitar la «pirámide de callbacks». Nadie las reutilizará fuera de la cadena de acción. Así que hay muchos nombres abarrotados aquí.

Nos gustaría tener algo mejor.

Afortunadamente, hay otras formas de evitar tales pirámides. Una de las mejores formas es usar «promesas», que se describirán en el próximo capítulo.

Related Post

Deja una respuesta

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