El autor seleccionó el Fondo de Ayuda COVID-19 para recibir una donación como parte del programa Escribir para Donaciones.
Introducción
En los primeros días de internet, los sitios web a menudo consistían en datos estáticos en una página HTML. Pero ahora que las aplicaciones web se han vuelto más interactivas y dinámicas, se ha vuelto cada vez más necesario realizar operaciones intensivas como hacer solicitudes de red externas para recuperar datos de API. Para manejar estas operaciones en JavaScript, un desarrollador debe usar técnicas de programación asíncrona.
Dado que JavaScript es un lenguaje de programación de un solo hilo con un modelo de ejecución síncrono que procesa una operación tras otra, solo puede procesar una declaración a la vez. Sin embargo, una acción como solicitar datos de una API puede llevar un tiempo indeterminado, dependiendo del tamaño de los datos solicitados, la velocidad de la conexión de red y otros factores. Si las llamadas a la API se realizaran de manera síncrona, el navegador no podría manejar ninguna entrada del usuario, como desplazarse o hacer clic en un botón, hasta que se complete esa operación. Esto se conoce como bloqueo.
Para evitar comportamientos de bloqueo, el entorno del navegador tiene muchas APIs web a las que JavaScript puede acceder que son asincrónicas, lo que significa que pueden ejecutarse en paralelo con otras operaciones en lugar de secuencialmente. Esto es útil porque permite al usuario seguir utilizando el navegador normalmente mientras se procesan las operaciones asincrónicas.
Como desarrollador de JavaScript, necesitas saber cómo trabajar con APIs web asincrónicas y manejar la respuesta o error de esas operaciones. En este artículo, aprenderás sobre el bucle de eventos, la forma original de lidiar con el comportamiento asincrónico a través de callbacks, la actualización de ECMAScript 2015 con la adición de promesas y la práctica moderna de usar async/await
.
Nota: Este artículo se enfoca en JavaScript del lado del cliente en el entorno del navegador. Los mismos conceptos son generalmente ciertos en el entorno de Node.js, sin embargo, Node.js utiliza sus propias APIs de C++ en lugar de las APIs web del navegador. Para más información sobre la programación asincrónica en Node.js, consulta Cómo escribir código asincrónico en Node.js.
El bucle de eventos
Esta sección explicará cómo JavaScript maneja el código asíncrono con el bucle de eventos. Primero se ejecutará una demostración del bucle de eventos en acción, y luego se explicarán los dos elementos del bucle de eventos: la pila y la cola.
El código JavaScript que no utiliza ninguna API web asíncrona se ejecutará de manera sincrónica, uno a la vez, secuencialmente. Esto se demuestra con este código de ejemplo que llama a tres funciones que imprimen un número en la consola:
En este código, se definen tres funciones que imprimen números con console.log()
.
A continuación, se escriben las llamadas a las funciones:
La salida se basará en el orden en que se llamaron las funciones: first()
, second()
, luego third()
:
Output1
2
3
Cuando se utiliza una API web asíncrona, las reglas se vuelven más complicadas. Una API integrada con la que puedes probar esto es setTimeout
, que establece un temporizador y realiza una acción después de un tiempo especificado. setTimeout
necesita ser asíncrono, de lo contrario, todo el navegador permanecería congelado durante la espera, lo que resultaría en una mala experiencia para el usuario.
Agrega setTimeout
a la función second
para simular una solicitud asíncrona:
setTimeout
toma dos argumentos: la función que se ejecutará de forma asíncrona y la cantidad de tiempo que esperará antes de llamar a esa función. En este código envolviste console.log
en una función anónima y la pasaste a setTimeout
, luego estableciste que la función se ejecute después de 0
milisegundos.
Ahora llama a las funciones, como lo hiciste antes:
Podrías esperar que con un setTimeout
establecido en 0
milisegundos, al ejecutar estas tres funciones todavía se imprimirían los números en orden secuencial. Pero debido a que es asíncrono, la función con el temporizador se imprimirá al final:
Output1
3
2
Ya sea que establezcas el tiempo de espera en cero segundos o cinco minutos, no hará ninguna diferencia: el console.log
llamado por código asíncrono se ejecutará después de las funciones síncronas de nivel superior. Esto sucede porque el entorno hospedador de JavaScript, en este caso el navegador, utiliza un concepto llamado el bucle de eventos para manejar la concurrencia o eventos paralelos. Dado que JavaScript solo puede ejecutar una instrucción a la vez, necesita que el bucle de eventos sea informado de cuándo ejecutar qué instrucción específica. El bucle de eventos maneja esto con los conceptos de una pila y una cola.
Pila
El stack, o pila de llamadas, mantiene el estado de qué función se está ejecutando actualmente. Si no estás familiarizado con el concepto de una pila, puedes imaginarla como un array con propiedades de “Último en entrar, primero en salir” (LIFO), lo que significa que solo puedes añadir o eliminar elementos del final de la pila. JavaScript ejecutará el marco actual (o llamada de función en un entorno específico) en la pila, luego lo eliminará y pasará al siguiente.
Para el ejemplo que contiene solo código síncrono, el navegador maneja la ejecución en el siguiente orden:
- Agregar
first()
a la pila, ejecutarfirst()
que registra1
en la consola, eliminarfirst()
de la pila. - Agregar
second()
a la pila, ejecutarsecond()
que registra2
en la consola, eliminarsecond()
de la pila. - Agregar
third()
a la pila, ejecutarthird()
que registra3
en la consola, eliminarthird()
de la pila.
El segundo ejemplo con setTimeout
se ve así:
- Agregar
first()
a la pila, ejecutarfirst()
que registra1
en la consola, eliminarfirst()
de la pila. - Agregar
second()
a la pila, ejecutarsecond()
.- Agregar
setTimeout()
a la pila, ejecutar la API websetTimeout()
que inicia un temporizador y añade la función anónima a la cola, eliminarsetTimeout()
de la pila.
- Agregar
- Elimine
second()
de la pila. - Agregue
third()
a la pila, ejecutethird()
que registra3
en la consola, eliminethird()
de la pila. - El bucle de eventos verifica la cola en busca de mensajes pendientes y encuentra la función anónima de
setTimeout()
, agrega la función a la pila que registra2
en la consola, luego la elimina de la pila.
Usar setTimeout
, una API web asíncrona, introduce el concepto de la cola, que este tutorial cubrirá a continuación.
Cola
La cola, también conocida como cola de mensajes o cola de tareas, es un área de espera para funciones. Cuando la pila de llamadas está vacía, el bucle de eventos verificará la cola en busca de mensajes en espera, comenzando desde el mensaje más antiguo. Una vez que encuentra uno, lo agregará a la pila, que ejecutará la función en el mensaje.
En el ejemplo de setTimeout
, la función anónima se ejecuta inmediatamente después del resto de la ejecución de nivel superior, ya que el temporizador se estableció en 0
segundos. Es importante recordar que el temporizador no significa que el código se ejecutará exactamente en 0
segundos o en el tiempo especificado, sino que agregará la función anónima a la cola en esa cantidad de tiempo. Este sistema de cola existe porque si el temporizador agregara la función anónima directamente a la pila cuando el temporizador finaliza, interrumpiría cualquier función que se esté ejecutando actualmente, lo que podría tener efectos no deseados e impredecibles.
Nota: También hay otra cola llamada la cola de trabajos o cola de microtareas que maneja las promesas. Las microtareas como las promesas se manejan con una prioridad más alta que las macrotareas como setTimeout
.
Ahora sabes cómo el bucle de eventos utiliza la pila y la cola para manejar el orden de ejecución del código. La próxima tarea es descubrir cómo controlar el orden de ejecución en tu código. Para hacer esto, primero aprenderás sobre la forma original de asegurar que el código asíncrono se maneje correctamente por el bucle de eventos: las funciones de devolución de llamada.
Funciones de Devolución de Llamada
En el ejemplo de setTimeout
, la función con el temporizador se ejecutó después de todo en el contexto de ejecución principal de nivel superior. Pero si quisieras asegurarte de que una de las funciones, como la función third
, se ejecutara después del temporizador, entonces tendrías que usar métodos de codificación asíncrona. El temporizador aquí puede representar una llamada de API asíncrona que contiene datos. Quieres trabajar con los datos de la llamada de API, pero tienes que asegurarte de que los datos se devuelvan primero.
La solución original para lidiar con este problema es usar funciones de devolución de llamada. Las funciones de devolución de llamada no tienen una sintaxis especial; solo son una función que se ha pasado como argumento a otra función. La función que toma otra función como argumento se llama una función de orden superior. Según esta definición, cualquier función puede convertirse en una función de devolución de llamada si se pasa como argumento. Las devoluciones de llamada no son asíncronas por naturaleza, pero se pueden usar con fines asíncronos.
Aquí tienes un ejemplo de código sintáctico de una función de orden superior y una devolución de llamada:
En este código, defines una función fn
, defines una función higherOrderFunction
que toma una función callback
como argumento y pasas fn
como una devolución de llamada a higherOrderFunction
.
Ejecutar este código dará lo siguiente:
OutputJust a function
Volviendo al primer
, segundo
y tercer
funciones con setTimeout
. Esto es lo que tienes hasta ahora:
La tarea es hacer que la ejecución de la tercer
función siempre se retrase hasta que se complete la acción asíncrona en la segundo
función. Aquí es donde entran los callbacks. En lugar de ejecutar primer
, segundo
y tercer
a nivel superior de ejecución, pasarás la función tercer
como argumento a segundo
. La función segundo
ejecutará el callback después de que la acción asíncrona haya terminado.
Aquí están las tres funciones con un callback aplicado:
Ahora, ejecuta primer
y segundo
, luego pasa tercer
como argumento a segundo
:
Después de ejecutar este bloque de código, recibirás la siguiente salida:
Output1
2
3
Primero se imprimirá 1
, y después de que el temporizador se complete (en este caso, cero segundos, pero puedes cambiarlo a cualquier cantidad) se imprimirá 2
luego 3
. Al pasar una función como callback, has retrasado con éxito la ejecución de la función hasta que se complete la API web asíncrona (setTimeout
).
La lección clave aquí es que las funciones de devolución de llamada no son asíncronas—setTimeout
es la API web asíncrona responsable de manejar tareas asíncronas. La devolución de llamada simplemente te permite ser informado de cuándo ha completado una tarea asíncrona y maneja el éxito o el fracaso de la tarea.
Ahora que has aprendido cómo usar devoluciones de llamada para manejar tareas asíncronas, la siguiente sección explica los problemas de anidar demasiadas devoluciones de llamada y crear una “pirámide del caos.”
Devoluciones de Llamada Anidadas y la Pirámide del Caos
Las funciones de devolución de llamada son una forma efectiva de asegurar la ejecución retrasada de una función hasta que otra se complete y devuelva datos. Sin embargo, debido a la naturaleza anidada de las devoluciones de llamada, el código puede terminar siendo desordenado si tienes muchas solicitudes asíncronas consecutivas que dependen entre sí. Esto fue una gran frustración para los desarrolladores de JavaScript al principio, y como resultado, el código que contiene devoluciones de llamada anidadas a menudo se llama “pirámide del caos” o “infierno de devolución de llamada.”
Aquí hay una demostración de devoluciones de llamada anidadas:
En este código, cada nuevo setTimeout
está anidado dentro de una función de orden superior, creando una forma de pirámide de devoluciones de llamada cada vez más profundas. Ejecutar este código daría como resultado lo siguiente:
Output1
2
3
En la práctica, con código asíncrono del mundo real, esto puede volverse mucho más complicado. Lo más probable es que necesites manejar errores en el código asíncrono y luego pasar algunos datos de cada respuesta a la siguiente solicitud. Hacer esto con devoluciones de llamada hará que tu código sea difícil de leer y mantener.
Aquí tienes un ejemplo ejecutable de un “pirámide del infierno” más realista con el que puedes experimentar:
En este código, debes hacer que cada función tenga en cuenta una posible respuesta
y un posible error
, lo que hace que la función callbackHell
sea visualmente confusa.
Al ejecutar este código, obtendrás lo siguiente:
Output
First 9
Second 3
Error: Whoa! Something went wrong.
at asynchronousRequest (<anonymous>:4:21)
at second (<anonymous>:29:7)
at <anonymous>:9:13
Esta forma de manejar código asíncrono es difícil de seguir. Como resultado, se introdujo el concepto de promesas en ES6. Esto es el foco de la siguiente sección.
Promesas
A promise represents the completion of an asynchronous function. It is an object that might return a value in the future. It accomplishes the same basic goal as a callback function, but with many additional features and a more readable syntax. As a JavaScript developer, you will likely spend more time consuming promises than creating them, as it is usually asynchronous Web APIs that return a promise for the developer to consume. This tutorial will show you how to do both.
Creando una Promesa
Puedes inicializar una promesa con la sintaxis new Promise
, y debes inicializarla con una función. La función que se pasa a una promesa tiene los parámetros resolve
y reject
. Las funciones resolve
y reject
manejan el éxito y el fracaso de una operación, respectivamente.
Escribe la siguiente línea para declarar una promesa:
Si inspeccionas la promesa inicializada en este estado con la consola del navegador web, encontrarás que tiene un estado pending
y un valor undefined
:
Output__proto__: Promise
[[PromiseStatus]]: "pending"
[[PromiseValue]]: undefined
Hasta ahora, no se ha configurado nada para la promesa, así que va a permanecer en un estado pending
para siempre. Lo primero que puedes hacer para probar una promesa es cumplirla resolviéndola con un valor:
Ahora, al inspeccionar la promesa, encontrarás que tiene un estado de fulfilled
, y un value
establecido en el valor que pasaste a resolve
:
Output__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: "We did it!"
Como se indicó al principio de esta sección, una promesa es un objeto que puede devolver un valor. Después de cumplirse con éxito, el value
pasa de ser undefined
a estar poblado con datos.
A promise can have three possible states: pending, fulfilled, and rejected.
- Pendiente – Estado inicial antes de ser resuelta o rechazada
- Cumplido – Operación exitosa, promesa ha sido resuelta
- Rechazado – Operación fallida, promesa ha sido rechazada
Después de ser cumplida o rechazada, una promesa está resuelta.
Ahora que tienes una idea de cómo se crean las promesas, veamos cómo un desarrollador puede consumirlas.
Consumiendo una Promesa
La promesa en la última sección se ha cumplido con un valor, pero también deseas poder acceder al valor. Las promesas tienen un método llamado then
que se ejecutará después de que una promesa alcance resolve
en el código. then
devolverá el valor de la promesa como parámetro.
Así es como devolverías y registrarías el valor
de la promesa de ejemplo:
La promesa que creaste tenía un [[PromiseValue]]
de ¡Lo logramos!
. Este valor es el que se pasará a la función anónima como respuesta
:
OutputWe did it!
Hasta ahora, el ejemplo que creaste no implicaba una API web asíncrona, solo explicaba cómo crear, resolver y consumir una promesa nativa de JavaScript. Usando setTimeout
, puedes probar una solicitud asíncrona.
El siguiente código simula datos devueltos de una solicitud asíncrona como una promesa:
Usar la sintaxis then
asegura que la respuesta
se registre solo cuando la operación setTimeout
se complete después de 2000
milisegundos. Todo esto se hace sin anidar devoluciones de llamada.
Ahora, después de dos segundos, resolverá el valor de la promesa y se registrará en then
:
OutputResolving an asynchronous request!
Las promesas también pueden encadenarse para pasar datos a más de una operación asincrónica. Si se devuelve un valor en then
, se puede agregar otro then
que se cumplirá con el valor de retorno del then
anterior:
La respuesta cumplida en el segundo then
registrará el valor de retorno:
OutputResolving an asynchronous request! And chaining!
Dado que then
puede encadenarse, permite que el consumo de promesas parezca más sincrónico que las devoluciones de llamada, ya que no es necesario anidarlas. Esto permitirá un código más legible que pueda mantenerse y verificarse más fácilmente.
Manejo de errores
Hasta ahora, solo has manejado una promesa con un resolve
exitoso, lo que coloca la promesa en un estado fulfilled
. Pero frecuentemente, con una solicitud asíncrona, también tienes que manejar un error: si la API está caída, o se envía una solicitud malformada o no autorizada. Una promesa debería poder manejar ambos casos. En esta sección, crearás una función para probar tanto el caso de éxito como el de error al crear y consumir una promesa.
Esta función getUsers
pasará un indicador a una promesa y devolverá la promesa:
Configura el código para que si onSuccess
es true
, el tiempo de espera se cumpla con algunos datos. Si es false
, la función rechazará con un error:
Para el resultado exitoso, devuelve objetos JavaScript que representan datos de usuario de muestra.
Para manejar el error, usarás el método de instancia catch
. Esto te dará un callback de fallo con el error
como parámetro.
Ejecuta el comando getUser
con onSuccess
establecido en false
, utilizando el método then
para el caso de éxito y el método catch
para el error:
Desde que se desencadenó el error, se omitirá el then
y el catch
manejará el error:
OutputFailed to fetch data!
Si cambias la bandera y en su lugar resolve
, el catch
será ignorado y en su lugar se devolverá los datos:
Esto proporcionará los datos del usuario:
Output(3) [{…}, {…}, {…}]
0: {id: 1, name: "Jerry"}
1: {id: 2, name: "Elaine"}
3: {id: 3, name: "George"}
Para referencia, aquí hay una tabla con los métodos de manejo en objetos Promise
:
Method | Description |
---|---|
then() |
Handles a resolve . Returns a promise, and calls onFulfilled function asynchronously |
catch() |
Handles a reject . Returns a promise, and calls onRejected function asynchronously |
finally() |
Called when a promise is settled. Returns a promise, and calls onFinally function asynchronously |
Las Promesas pueden ser confusas, tanto para nuevos desarrolladores como para programadores experimentados que nunca han trabajado en un entorno asíncrono. Sin embargo, como se mencionó, es mucho más común consumir promesas que crearlas. Por lo general, una API web del navegador o una biblioteca de terceros proporcionarán la promesa, y solo necesitas consumirla.
En la sección final sobre promesas, este tutorial citará un caso de uso común de una API web que devuelve promesas: la API Fetch.
Usando la API Fetch con Promesas
Una de las APIs web más útiles y frecuentemente utilizadas que devuelve una promesa es la Fetch API, que te permite realizar una solicitud de recurso asíncrona a través de una red. `fetch` es un proceso de dos partes y, por lo tanto, requiere encadenar `then`. Este ejemplo demuestra cómo obtener datos de usuario de la API de GitHub, mientras maneja cualquier posible error:
La solicitud `fetch` se envía a la URL `https://api.github.com/users/octocat`, que espera de manera asíncrona una respuesta. El primer `then` pasa la respuesta a una función anónima que formatea la respuesta como datos JSON, luego pasa el JSON a un segundo `then` que registra los datos en la consola. La declaración `catch` registra cualquier error en la consola.
Al ejecutar este código, se obtendrá lo siguiente:
Outputlogin: "octocat",
id: 583231,
avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4"
blog: "https://github.blog"
company: "@github"
followers: 3203
...
Estos son los datos solicitados desde `https://api.github.com/users/octocat`, representados en formato JSON.
Esta sección del tutorial mostró que las promesas incorporan muchas mejoras para lidiar con código asíncrono. Sin embargo, aunque usar `then` para manejar acciones asíncronas es más fácil de seguir que la pirámide de devoluciones de llamada, algunos desarrolladores aún prefieren un formato síncrono para escribir código asíncrono. Para abordar esta necesidad, ECMAScript 2016 (ES7) introdujo funciones `async` y la palabra clave `await` para facilitar el trabajo con promesas.
Funciones asíncronas con async/await
Una función async
te permite manejar código asíncrono de manera que parece sincrónico. Las funciones async
siguen utilizando promesas internamente, pero tienen una sintaxis más tradicional de JavaScript. En esta sección, probarás ejemplos de esta sintaxis.
Puedes crear una función async
agregando la palabra clave async
antes de una función:
Aunque esta función aún no maneja nada asíncrono, se comporta de manera diferente a una función tradicional. Si ejecutas la función, verás que devuelve una promesa con un [[PromiseStatus]]
y [[PromiseValue]]
en lugar de un valor de retorno.
Prueba esto registrando una llamada a la función getUser
:
Esto te dará lo siguiente:
Output__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: Object
Esto significa que puedes manejar una función async
con then
de la misma manera que podrías manejar una promesa. Pruébalo con el siguiente código:
Esta llamada a getUser
pasa el valor de retorno a una función anónima que registra el valor en la consola.
Recibirás lo siguiente cuando ejecutes este programa:
Output{}
Una función async
puede manejar una promesa llamada dentro de ella utilizando el operador await
. await
se puede usar dentro de una función async
y esperará hasta que una promesa se resuelva antes de ejecutar el código designado.
Con este conocimiento, puedes reescribir la solicitud Fetch de la última sección usando async
/await
de la siguiente manera:
Los operadores await
aquí aseguran que los datos no se registren antes de que la solicitud los haya poblado con datos.
Ahora los datos finales pueden manejarse dentro de la función getUser
, sin necesidad de usar then
. Este es el resultado de registrar data
:
Outputlogin: "octocat",
id: 583231,
avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4"
blog: "https://github.blog"
company: "@github"
followers: 3203
...
Nota: En muchos entornos, es necesario usar async
para utilizar await
, sin embargo, algunas nuevas versiones de navegadores y Node permiten usar await
de nivel superior, lo que te permite evitar crear una función async para envolver el await
.
Finalmente, dado que estás manejando la promesa cumplida dentro de la función asíncrona, también puedes manejar el error dentro de la función. En lugar de usar el método catch
con then
, usarás el patrón try
/catch
para manejar la excepción.
Agrega el siguiente código resaltado:
El programa ahora saltará al bloque catch
si recibe un error y registrará ese error en la consola.
El código asincrónico moderno en JavaScript se maneja más a menudo con la sintaxis async
/await
, pero es importante tener un conocimiento funcional de cómo funcionan las promesas, especialmente porque las promesas son capaces de características adicionales que no se pueden manejar con async
/await
, como combinar promesas con Promise.all()
.
Nota: async
/await
se puede reproducir usando generadores combinados con promesas para agregar más flexibilidad a tu código. Para aprender más, consulta nuestro tutorial Entendiendo Generadores en JavaScript.
Conclusión
Porque las API web a menudo proporcionan datos de forma asincrónica, aprender a manejar el resultado de acciones asíncronas es una parte esencial de ser un desarrollador de JavaScript. En este artículo, aprendiste cómo el entorno de alojamiento utiliza el bucle de eventos para manejar el orden de ejecución del código con la pila y la cola. También probaste ejemplos de tres formas de manejar el éxito o el fracaso de un evento asíncrono, con devoluciones de llamada, promesas y sintaxis async
/await
. Finalmente, utilizaste la API web Fetch para manejar acciones asíncronas.
Para obtener más información sobre cómo el navegador maneja eventos paralelos, lee Modelo de concurrencia y el bucle de eventos en la Red de Desarrolladores de Mozilla. Si deseas aprender más sobre JavaScript, regresa a nuestra serie Cómo Programar en JavaScript.