10º junio 2020

Introducción: funciones de retrollamadas (callbacks)

Usamos métodos de navegador en estos ejemplos

Para demostrar el uso de callbacks, promesas y otros conceptos abstractos, utilizaremos algunos métodos de navegador: específicamente, carga de scripts y simples manipulaciones de documentos.

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

Sin embargo, intentaremos aclarar las cosas de todos modos. No habrá nada en cuanto al navegador realmente complejo.

Muchas funciones son proporcionadas por el entorno de host de Javascript que permiten programar acciones asíncronas. En otras palabras, acciones que iniciamos ahora, pero que terminan más tarde.

Por ejemplo, una de esas funciones es la función setTimeout.

Hay otros ejemplos del mundo real de acciones asincrónicas, p. ej.: la carga de scripts y módulos (a cubrirse en capítulos posteriores).

Echa un vistazo a la función loadScript(src), que carga un script dado: src

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

Anexa al documento la nueva etiqueta, creada dinámicamente, <script src =" ... "> con el src dado. El navegador comienza a cargarlo automáticamente y se ejecuta cuando se completa.

Esta función la podemos usar así:

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

El script se ejecuta “asincrónicamente”, ya que comienza a cargarse ahora, pero se ejecuta más tarde, cuando la función ya ha finalizado.

El código debajo de loadScript (...), no espera que finalice la carga del script.

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

Digamos que necesitamos usar el nuevo script tan pronto como se cargue. Declara nuevas funciones, y queremos ejecutarlas.

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

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

newFunction(); // no hay dicha función!

Naturalmente, el navegador probablemente no tuvo tiempo de cargar el script. Hasta el momento, la función loadScript no proporciona una forma de rastrear la finalización de la carga. El script se carga y finalmente se ejecuta, eso es todo. Pero nos gustaría saber cuándo sucede, para usar las funciones y variables nuevas de dicho script.

Agreguemos una función callback como segundo argumento para loadScript que debería ejecutarse cuando se carga el script:

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

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

  document.head.append(script);
}

Ahora, si queremos llamar las nuevas funciones desde el script, deberíamos escribirlo en la callback:

loadScript('/my/script.js', function() {
  // la callback se ejecuta luego que se carga el script
  newFunction(); // ahora funciona
  ...
});

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

Aquí 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( _ ); // función declarada en el script cargado
});

Eso se llama programación asincrónica “basado en callback”. Una función que hace algo de forma asincrónica debería aceptar un argumento de callback donde ponemos la función por ejecutar después de que se complete.

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

Callback en una callback

¿Cómo podemos cargar dos scripts secuencialmente: el primero y después el segundo al cargarse el primero?

La solución natural sería poner la segunda llamada loadScript dentro de la callback, 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 un script más …?

loadScript('/my/script.js', function(script) {

  loadScript('/my/script2.js', function(script) {

    loadScript('/my/script3.js', function(script) {
      // ...continua despues que se han cargado todos los scripts
    });

  })

});

Entonces, cada nueva acción está dentro de una callback. Eso está bien para algunas acciones, pero no es bueno para todas, así 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í una versión mejorada de loadScript que rastrea 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 de carga de script con ${src}`));

  document.head.append(script);
}

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

El uso:

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. Se llama el estilo de “callback error primero”.

La convención es:

  1. El primer argumento de la ‘callback’ está reservado para un error, si ocurre. Entonces se llama a callback(err).
  2. El segundo argumento (y los siguientes si es necesario) son para el resultado exitoso. Entonces se llama a callback(null, result1, result2 ...).

Por lo tanto, la única función de ‘callback’ se usa tanto para informar errores como para transferir resultados.

Pirámide de funciones callback

A primvera vista, es 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 van 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 {
            // ...continua después que se han cargado todos los script (*)
          }
        });

      }
    })
  }
});

En el código de arriba:

  1. Cargamos 1.js, entonces si no hay error.
  2. Cargamos 2.js, entonces si no hay error.
  3. Cargamos 3.js, luego, si no hay ningún error, haga 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 “callback hell” o “Pirámide de Doom”.

La “pirámide” de llamadas anidadas crece a la derecha con cada acción asincrónica. Pronto se sale de control.

Entonces, esta forma de codificación no es tan buena.

Trataremos de aliviar el problema haciendo cada acción una función independiente, asi:

loadScript('1.js', step1);

function step1(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', step2);
  }
}

function step2(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('3.js', step3);
  }
}

function step3(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...continua despues 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 una hoja de cálculo desgarrada. Es difícil de leer, y habrías 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 dirigirse.

Además, las funciones llamadas step* son de un solo uso, son para evitar la “Pirámide de funciones callback”. Nadie los reutilizará fuera de la cadena de acción. Así que hay muchos nombres abarrotados aquí.

Nos gustaría tener algo mejor.

Afortunadamente, podemos evitar tales pirámides. Una de las mejores formas es usando “promesas”, descritas en el próximo capítulo.

Tareas

En la tarea Círculo animado se muestra un círculo creciente animado.

Ahora digamos que necesitamos no solo un círculo, sino mostrar un mensaje dentro de él. El mensaje debería aparecer después de que la animación esté completa (el círculo es desarrollado completamente), de lo contrario se vería feo.

En la solución de la tarea, la función showCircle(cx, cy, radius) dibuja el círculo, pero no hay forma de saber cuando lo termina.

Agrega un argumento callback: showCircle(cx, cy, radius, callback) que se llamará cuando se complete la animación. El callback debería recibir el círculo <div> como argumento.

Aqui el ejemplo:

showCircle(150, 150, 100, div => {
  div.classList.add('message-ball');
  div.append("Hola, mundo!");
});

Demostración:

Toma la solución de la tarea Círculo animado como base.

Mapa del Tutorial

Comentarios

lea esto antes de comentar…
  • Si tiene sugerencias sobre qué mejorar, por favor enviar una propuesta de GitHub o una solicitud de extracción en lugar de comentar.
  • Si no puede entender algo en el artículo, por favor explique.
  • Para insertar algunas palabras de código, use la etiqueta <code>, para varias líneas – envolverlas en la etiqueta <pre>, para más de 10 líneas – utilice una entorno controlado (sandbox) (plnkr, jsbin, codepen…)