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 aremoveEventListener
o, 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 Promise
s. 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 Promise
s:
-
Los controladores de eventos se disparan varias veces, mientras que una llamada de retorno
then
se ejecuta una sola vez. Puedes pensar enPromise
como una operación que se realiza una sola vez. -
Si llamas a
then
en unPromise
, 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. -
Promise
s 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
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
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 elPromise
con elImage
, 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 atributosrc
en la URL alternativa y el navegador intenta cargar la imagen alternativa. Si lo consigue, se dispara el eventoload
y resuelve elPromise
con la URL de reservaImage
. - 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 eventoerror
.
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
Solución
Utiliza una cadena de Promise
s para ejecutar las tareas asíncronas en secuencia. Imagina una aplicación de blog con dos APIs, ambas devuelven Promise
s:
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 Promise
s.
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
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 Promise
s. 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
Solución
Recoge todos los Promise
s y pásalos a Promise.all
. Esta función toma una matriz de Promise
s y espera a que se completen todos. Devuelve un nuevo Promise
que se cumple cuando se cumplen todos los Promise
s dados, o rechaza si se rechaza alguno de los Promise
s 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 Promise
s 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 Promise
s 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 Promise
s 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 Promise
s (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
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 requestAnimationFrame
(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
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.