Capítulo 1. API asíncronas

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Introducción

A muchas de las API que se tratan en este libro son asíncronas. Cuando llamas a una de estas funciones o métodos, es posible que no obtengas el resultado inmediatamente. Las distintas API tienen diferentes mecanismos para devolverte el resultado cuando esté listo.

Funciones de rellamada

El patrón asíncrono más básico es una función de devolución de llamada. Se trata de una función que pasas a una API asíncrona. Cuando finaliza el trabajo, llama a tu llamada de retorno con el resultado. Las retrollamadas pueden utilizarse solas o como parte de otros patrones asíncronos.

Eventos

Muchas API de los navegadores se basan en eventos. Un evento es algo que ocurre de forma asíncrona. Algunos ejemplos de eventos son:

  • Se pulsó un botón.

  • El ratón se movió.

  • Se ha completado una solicitud de red.

  • Se ha producido un error.

Un evento tiene un nombre, como click o mouseover, y un objeto con datos sobre el evento ocurrido. Puede incluir información como qué elemento se pulsó o un código de estado HTTP. Cuando escuchas un evento, proporcionas una función de devolución de llamada que recibe el objeto del evento como argumento.

Los objetos que emiten eventos implementan la interfaz EventTarget, que proporciona los métodos addEventListener y removeEventListener. Para escuchar un evento en un elemento u otro objeto, puedes llamar a addEventListener sobre él, pasándole el nombredel evento y una función manejadora. Se llamará a la llamada de retorno cada vez quese active el evento hasta que se elimine. Un receptor puede eliminarse manualmente llamando aremoveEventListenero, en muchos casos, el navegador elimina automáticamente los escuchadores cuando los objetos se destruyen o se eliminan del DOM.

Promesas

Muchas API más recientes utilizan Promises. Un Promise es un objeto, devuelto por una función, que es un marcador de posición para el resultado final de la acción asíncrona. En lugar de esperar un evento, llamas a then sobre un objeto Promise. Pasas una función de llamada de retorno a then que finalmente se llama con el resultado como argumento. Para gestionar los errores, pasas otra función de devolución de llamada al método catch de Promise.

Un Promise se cumple cuando la operación se completa con éxito, y se rechaza cuando hay un error. El valor cumplido se pasa como argumento a la llamada de retorno then, o el valor rechazado se pasa como argumento a la llamada de retorno catch.

Hay algunas diferencias clave entre los eventos y Promises:

  • Los controladores de eventos se disparan varias veces, mientras que una llamada de retorno then se ejecuta una sola vez. Puedes pensar en Promise como una operación que se realiza una sola vez.

  • Si llamas a then en un Promise, siempre obtendrás el resultado (si lo hay). Esto es diferente de los eventos, en los que, si se produce un evento antes de que añadas un oyente, el evento se pierde.

  • Promises tienen incorporado un mecanismo de gestión de errores. Con los eventos, normalmente necesitas escuchar un evento de error para gestionar las condiciones de error.

Trabajar con promesas

Problema

Quieres llamar a una API que utiliza Promises y recuperar el resultado.

Solución

Llama a then en el objeto Promise para manejar el resultado en una función de devolución de llamada. Para gestionar posibles errores, añade una llamada a catch.

Imagina que tienes una función getUsers que realiza una petición de red para cargar una lista de usuarios. Esta función devuelve un Promise que finalmente resuelve la lista de usuarios (ver Ejemplo 1-1).

Ejemplo 1-1. Utilizar una API basada en Promise
getUsers()
  .then(
    // This function is called when the user list has been loaded.
    userList => {
      console.log('User List:');
      userList.forEach(user => {
        console.log(user.name);
      });
    }
  ).catch(error => {
    console.error('Failed to load the user list:', error);
  });

Debate

El Promise devuelto por getUsers es un objeto con un método then. Cuando se carga la lista de usuarios, se ejecuta la llamada de retorno pasada a then con la lista de usuarios comoargumento.

Este Promise también tiene un método catch para gestionar errores. Si se produce un error al cargar la lista de usuarios, se llama a la llamada de retorno pasada a catch con el objeto de error. Sólo se llama a una de estas llamadas de retorno, dependiendo del resultado.

Cargar una imagen con un Fallback

Problema

Quieres cargar una imagen para mostrarla en la página. Si se produce un error al cargar la imagen, quieres utilizar una URL de imagen conocida como alternativa.

Solución

Crea un elemento Image mediante programación, y escucha sus eventos load y error. Si se activa el evento error, sustitúyelo por la imagen de reserva. Una vez cargada la imagen solicitada o la imagen de reserva, añádela al DOM cuando lo desees.

Para una API más limpia, puedes envolver esto en un Promise. El Promise resuelve con un Image que debe añadirse o rechaza con un error si no pueden cargarse ni la imagen ni el fallback (ver Ejemplo 1-2).

Ejemplo 1-2. Cargar una imagen con un fallback
/**
 * Loads an image. If there's an error loading the image, uses a fallback
 * image URL instead.
 *
 * @param url The image URL to load
 * @param fallbackUrl The fallback image to load if there's an error
 * @returns a Promise that resolves to an Image element to insert into the DOM
 */
function loadImage(url, fallbackUrl) {
  return new Promise((resolve, reject) => {
    const image = new Image();

    // Attempt to load the image from the given URL
    image.src = url;

    // The image triggers the 'load' event when it is successfully loaded.
    image.addEventListener('load', () => {
      // The now-loaded image is used to resolve the Promise
      resolve(image);
    });

    // If an image failed to load, it triggers the 'error' event.
    image.addEventListener('error', error => {
      // Reject the Promise in one of two scenarios:
      // (1) There is no fallback URL.
      // (2) The fallback URL is the one that failed.
      if (!fallbackUrl || image.src === fallbackUrl) {
        reject(error);
      } else {
        // If this is executed, it means the original image failed to load.
        // Try to load the fallback.
        image.src = fallbackUrl;
      }
    });
  });
}

Debate

La función loadImage toma una URL y una URL alternativa y devuelve un Promise. A continuación, crea un nuevo Image y establece su atributo src en la URL dada. El navegador intenta cargar la imagen.

Hay tres resultados posibles:

Caso de éxito

Si la imagen se carga correctamente, se activa el evento load. El controlador del evento resuelve el Promise con el Image, que puede entonces insertarse en el DOM.

Caso alternativo

Si la imagen no se carga, se activa el evento error. El controlador de errores establece el atributo src en la URL alternativa y el navegador intenta cargar la imagen alternativa. Si lo consigue, se dispara el evento load y resuelve el Promise con la URL de reserva Image.

Caso de fallo

Si no se han podido cargar ni la imagen ni la imagen de reserva, el gestor de errores rechaza la Promise con el evento error.

El evento error se activa cada vez que se produce un error de carga. El controlador comprueba primero si es la URL alternativa la que ha fallado. Si es así, significa que tanto la URL original como la URL alternativa no se han cargado. Este es el caso de fallo, por lo que se rechaza el Promise.

Si no es la URL alternativa, significa que la URL solicitada no se ha cargado. Ahora establece la URL alternativa e intenta cargarla.

El orden de las comprobaciones aquí es importante. Sin esa primera comprobación, si la URL alternativa no se carga, el controlador de errores activaría un bucle infinito de establecimiento de la URL alternativa (no válida), solicitud de la misma y activación del evento error de nuevo.

El ejemplo 1-3 muestra cómo utilizar esta función loadImage .

Ejemplo 1-3. Utilizar la función loadImage
loadImage('https://example.com/profile.jpg', 'https://example.com/fallback.jpg')
  .then(image => {
    // container is an element in the DOM where the image will go
    container.appendChild(image);
  }).catch(error => {
    console.error('Image load failed');
  });

Encadenar promesas

Problema

Quieres llamar secuencialmente a varias API basadas en Promise. Cada operación depende del resultado de la anterior.

Solución

Utiliza una cadena de Promises para ejecutar las tareas asíncronas en secuencia. Imagina una aplicación de blog con dos APIs, ambas devuelven Promises:

getUser(id)

Carga un usuario con el ID de usuario dado

getPosts(user)

Carga todas las entradas del blog de un usuario determinado

Si quieres cargar las entradas de un usuario, primero tienes que cargar el objeto user; no puedes llamar a getPosts hasta que se hayan cargado los detalles del usuario. Puedes hacerlo encadenando los dos Promise, como se muestra en el Ejemplo 1-4.

Ejemplo 1-4. Utilizar una cadena Promise
/**
 * Loads the post titles for a given user ID.
 * @param userId is the ID of the user whose posts you want to load
 * @returns a Promise that resolves to an array of post titles
 */
function getPostTitles(userId) {
  return getUser(userId)
    // Callback is called with the loaded user object
    .then(user => {
      console.log(`Getting posts for ${user.name}`);
      // This Promise is also returned from .then
      return getPosts(user);
    })
    // Calling then on the getPosts' Promise
    .then(posts => {
      // Returns another Promise that will resolve to an array of post titles
      return posts.map(post => post.title);
    })
    // Called if either getUser or getPosts are rejected
    .catch(error => {
      console.error('Error loading data:', error);
    });
}

Debate

El valor devuelto por un gestor de Promise's then se envuelve en un nuevo Promise. Este Promise se devuelve desde el propio método then. Esto significa que el valor devuelto por then es también un Promise, por lo que puedes encadenar otro then sobre él. Así es como se crea una cadena de Promises.

getUser devuelve un Promise que resuelve al objeto user. El manejador then llama a getPosts y devuelve el Promise resultante, que se devuelve de nuevo desde then, por lo que puedes llamar a then una vez más para obtener el resultado final, el array de posts.

Al final de la cadena hay una llamada a catch para gestionar cualquier error. Esto funciona como un bloque try/catch. Si se produce un error en cualquier punto de la cadena, se llama al gestor catch con ese error y no se ejecuta el resto de la cadena .

Uso de las palabras clave async y await

Problema

Estás trabajando con una API que devuelve un Promise, pero quieres que el código lea de forma más lineal, o sincrónica.

Solución

Utiliza la palabra clave await con la función Promise en lugar de llamar a then sobre ella (ver Ejemplo 1-5). Considera de nuevo la función getUsers de "Trabajar con promesas". Esta función devuelve un Promise que resuelve una lista de usuarios.

Ejemplo 1-5. Utilizar la palabra clave await
// A function must be declared with the async keyword
// in order to use await in its body.
async function listUsers() {
  try {
    // Equivalent to getUsers().then(...)
    const userList = await getUsers();
    console.log('User List:');
    userList.forEach(user => {
      console.log(user.name);
    });
  } catch (error) { // Equivalent to .catch(...)
    console.error('Failed to load the user list:', error);
  }
}

Debate

await es una sintaxis alternativa para trabajar con Promises. En lugar de llamar a then con una llamada de retorno que toma el resultado como argumento, la expresión "pausa" de hecho la ejecución del resto de la función y devuelve el resultado cuando se cumple Promise.

Si se rechaza Promise, la expresión await arroja el valor rechazado. Esto se gestiona con un bloque estándar try/catch.

Utilizar promesas en paralelo

Problema

Quieres ejecutar una serie de tareas asíncronas en paralelo utilizando Promises.

Solución

Recoge todos los Promises y pásalos a Promise.all. Esta función toma una matriz de Promises y espera a que se completen todos. Devuelve un nuevo Promise que se cumple cuando se cumplen todos los Promises dados, o rechaza si se rechaza alguno de los Promises dados (ver Ejemplo 1-6).

Ejemplo 1-6. Cargar varios usuarios con Promise.all
// Loading three users at once
Promise.all([
  getUser(1),
  getUser(2),
  getUser(3)
]).then(users => {
  // users is an array of user objects—the values returned from
  // the parallel getUser calls
}).catch(error => {
  // If any of the above Promises are rejected
  console.error('One of the users failed to load:', error);
});

Debate

Si tienes varias tareas que no dependen unas de otras, Promise.all es una buena opción. El Ejemplo 1-6 llama a getUser tres veces, pasando un ID de usuario diferente cada vez. Recoge estas Promises en una matriz que se pasa a Promise.all. Las tres peticiones se ejecutan en paralelo.

Promise.all devuelve otro Promise. Una vez que los tres usuarios se han cargado correctamente, este nuevo Promise se completa con una matriz que contiene los usuarios cargados. El índice de cada resultado corresponde al índice del Promise en la matriz de entrada. En este caso, devuelve una matriz con los usuarios 1, 2, y 3, en ese orden.

¿Qué ocurre si uno o varios de estos usuarios no se cargan? Puede que uno de los identificadores de usuario no exista o que se haya producido un error temporal en la red. Si alguno de los Promises pasados a Promise.all es rechazado, el nuevo Promise también lo rechaza inmediatamente. El valor de rechazo es el mismo que el del Promise rechazado.

Si uno de los usuarios no se carga, el Promise devuelto por Promise.all se rechaza con el error producido. Los resultados de los otros Promises se pierden.

Si aún quieres obtener los resultados de cualquier Promises resuelto (o los errores de otros rechazados), puedes utilizar en su lugar Promise.allSettled. Con Promise.allSettled, se devuelve un nuevo Promise igual que con Promise.all. Sin embargo, este Promise siempre se cumple, una vez resueltos todos los Promises (cumplidos o rechazados).

Como se muestra en el Ejemplo 1-7, el valor resuelto es una matriz cuyos elementos tienen cada uno una propiedad status. Ésta es fulfilled o rejected, dependiendo del resultado de esa Promise. Si el estado es fulfilled, el objeto también tiene una propiedad value que es el valor resuelto. En cambio, si el estado es rejected, tiene una propiedadreason que es el valor rechazado.

Ejemplo 1-7. Utilizando Promise.allSettled
Promise.allSettled([
  getUser(1),
  getUser(2),
  getUser(3)
]).then(results => {
  results.forEach(result => {
    if (result.status === 'fulfilled') {
      console.log('- User:', result.value.name);
    } else {
      console.log('- Error:', result.reason);
    }
  });
});
// No catch necessary here because allSettled is always fulfilled.

Animar un elemento con requestAnimationFrame

Problema

Quieres animar un elemento de forma eficaz utilizando JavaScript.

Solución

Utiliza la función requestAnimationFrame para programar las actualizaciones de tus animaciones para que se ejecuten a intervalos regulares.

Imagina que tienes un elemento div que quieres ocultar con una animación de desvanecimiento. Esto se hace ajustando la opacidad a intervalos regulares, mediante una llamada de retorno pasada a request​A⁠nimationFrame (ver Ejemplo 1-8). La duración de cada intervalo depende de los fotogramas por segundo (FPS) deseados de la animación.

Ejemplo 1-8. Animación de fundido de salida con requestAnimationFrame
const animationSeconds = 2; // Animate over 2 seconds
const fps = 60; // A nice, smooth animation

// The time interval between each frame
const frameInterval = 1000 / fps;

// The total number of frames for the animation
const frameCount = animationSeconds * fps;

// The amount to adjust the opacity by in each frame
const opacityIncrement = 1 / frameCount;

// The timestamp of the last frame
let lastTimestamp;

// The starting opacity value
let opacity = 1;

function fade(timestamp) {
  // Set the last timestamp to now if there isn't an existing one.
  if (!lastTimestamp) {
    lastTimestamp = timestamp;
  }

  // Calculate how much time has elapsed since the last frame.
  // If not enough time has passed yet, schedule another call of this
  // function and return.
  const elapsed = timestamp - lastTimestamp;
  if (elapsed < frameInterval) {
    requestAnimationFrame(animate);
    return;
  }

  // Time for a new animation frame. Remember this timestamp.
  lastTimestamp = timestamp;

  // Adjust the opacity value and make sure it doesn't go below 0.
  opacity = Math.max(0, opacity - opacityIncrement)
  box.style.opacity = opacity;

  // If the opacity hasn't reached the target value of 0, schedule another
  // call to this function.
  if (opacity > 0) {
    requestAnimationFrame(animate);
  }
}

// Schedule the first call to the animation function.
requestAnimationFrame(fade);

Debate

Esta es una forma buena y eficaz de animar elementos utilizando JavaScript con un buen soporte del navegador. Como se hace de forma asíncrona, esta animación no bloqueará el hilo principal del navegador. Si el usuario cambia a otra pestaña, la animación se detiene y no se llama innecesariamente a requestAnimationFrame.

Cuando programas una función para que se ejecute con requestAnimationFrame, se llama a la función antes de la siguiente operación de repintado. La frecuencia con la que esto ocurre depende del navegador y de la frecuencia de actualización de la pantalla.

Antes de animar, el Ejemplo 1-8 hace algunos cálculos basándose en una duración de la animación dada (2 segundos) y una velocidad de fotogramas (60 fotogramas por segundo). Calcula el número total de fotogramas, y utiliza la duración para calcular cuánto dura cada fotograma. Si quieres una velocidad de fotogramas diferente que no coincida con la velocidad de refresco del sistema, esto hace un seguimiento de cuándo se realizó la última actualización de la animación para mantener tu velocidad de fotogramas objetivo.

Luego, en función del número de fotogramas, calcula el ajuste de opacidad realizado en cada fotograma.

La función fade se programa pasándola a una llamada a requestAnimationFrame. Cada vez que el navegador llama a esta función, le pasa una marca de tiempo. La función fade calcula cuánto tiempo ha transcurrido desde el último fotograma. Si aún no ha pasado suficiente tiempo, no hace nada y pide al navegador que vuelva a llamar la próxima vez.

Una vez transcurrido el tiempo suficiente, realiza un paso de animación. Toma el ajuste de opacidad calculado y lo aplica al estilo del elemento. Dependiendo del tiempo exacto, esto podría dar como resultado una opacidad inferior a 0, que no es válida. Esto se soluciona utilizando Math.max para establecer un valor mínimo de 0.

Si la opacidad aún no ha llegado a 0, es necesario ejecutar más fotogramas de animación. Llama de nuevo a requestAnimationFrame para programar la siguiente ejecución.

Como alternativa a este método, los navegadores más recientes admiten la API de Animaciones Web, que conocerás en el Capítulo 8. Esta API te permite especificar fotogramas clave con propiedades CSS, y el navegador se encarga de actualizar los valores intermedios para que .

Envolver una API de eventos en una promesa

Problema

Quieres envolver una API basada en eventos para que devuelva un Promise.

Solución

Crea un nuevo objeto Promise y registra escuchadores de eventos dentro de su constructor. Cuando recibas el evento que estás esperando, resuelve el Promise con el valor. Del mismo modo, rechaza el Promise si se produce un evento de error.

A veces a esto se le llama "prometer" una función. En el Ejemplo 1-9 se muestra cómo prometer la API XMLHttpRequest.

Ejemplo 1-9. Promocionar la API XMLHttpRequest
/**
 * Sends a GET request to the specified URL. Returns a Promise that will resolve to
 * the JSON body parsed as an object, or will reject if there is an error or the
 * response is not valid JSON.
 *
 * @param url The URL to request
 * @returns a Promise that resolves to the response body
 */
function loadJSON(url) {
  // Create a new Promise object, performing the async work inside the
  // constructor function.
  return new Promise((resolve, reject) => {
    const request = new XMLHttpRequest();

    // If the request is successful, parse the JSON response and
    // resolve the Promise with the resulting object.
    request.addEventListener('load', event => {
      // Wrap the JSON.parse call in a try/catch block just in case
      // the response body is not valid JSON.
      try {
        resolve(JSON.parse(event.target.responseText));
      } catch (error) {
        // There was an error parsing the response body.
        // Reject the Promise with this error.
        reject(error);
      }
    });

    // If the request fails, reject the Promise with the
    // error that was emitted.
    request.addEventListener('error', error => {
      reject(error);
    });

    // Set the target URL and send the request.
    request.open('GET', url);
    request.send();
  });
}

El ejemplo 1-10 muestra cómo utilizar la función loadJSON prometida.

Ejemplo 1-10. Utilización del ayudante loadJSON
// Using .then
loadJSON('/api/users/1').then(user => {
  console.log('Got user:', user);
})

// Using await
const user = await loadJSON('/api/users/1');
console.log('Got user:', user);

Debate

Creas un Promise llamando a la función constructora Promise con el operador new. Esta función recibe dos argumentos, un resolve y una función reject.

Las funciones resolve y reject las proporciona el motor de JavaScript. Dentro del constructor Promise, haces tu trabajo asíncrono y escuchas los eventos. Cuando se llama a la función resolve, el Promise resuelve inmediatamente a ese valor. Llamar a reject funciona de la misma manera: rechaza Promise con el error.

Crear tu propio Promise puede ayudar en este tipo de situaciones, pero en general no suele ser necesario crearlos manualmente de este modo. Si una API ya devuelve un Promise, no necesitas envolverlo en tu propio Promise-sólo tienes que utilizarlo directamente .

Get Libro de recetas de la API web now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.