A lo largo de mi carrera he estado en muchas entrevistas de trabajo, tanto como entrevistador como candidato. He visto que mi parte de candidatos se desmorona cuando se me pide que codifique código asincrónico usando Promises y async / wait.
La programación asincrónica es la esencia de JavaScript, sin embargo, muchos desarrolladores no lo entienden realmente por alguna razón.
Claro, han trabajado con código asíncrono y lo reconocerán cuando lo vean. Saben más o menos cómo funciona. Pero muchos no pueden reproducirlo con precisión y no comprenden todos los detalles esenciales de implementación.
Muchos desarrolladores que he entrevistado están atrapados en este nivel, lo cual es realmente triste.
Si quieres ser un desarrollador de JavaScript serio, entonces la programación asincrónica debería ser una segunda naturaleza. Deberías saber esto como un panadero sabe pan.
La programación asincrónica puede ser complicada, pero no es ciencia espacial. Sólo hay algunas cosas que usted necesita saber.
Cuando domine estos conceptos básicos, no tendrá problemas para comprender los casos de uso más avanzados e implementarlos usted mismo.
Lo que ya sabes
Ya sabes cómo trabajar con Promise y async / wait, al menos eso espero. Si no lo hace, consulte primero estos artículos sobre MDN y luego vuelva.
Lo que deberías saber
Hay algunos patrones en la programación asincrónica en JavaScript que siguen regresando. Estas son soluciones prácticas y de uso múltiple que puede y probablemente aplicará muy a menudo y deberían ser herramientas estándar en su caja de herramientas de JavaScript.
Convertir código basado en devolución de llamada en promesas
El código basado en la devolución de llamada puede ser engorroso para trabajar, especialmente cuando están involucradas múltiples llamadas encadenadas y manejo de errores. Esta es básicamente la razón por la que existen las promesas, para facilitar la programación asincrónica.
Es bastante sencillo convertir el código basado en devolución de llamada en código que usa Promesas.
Echemos un vistazo al código Node.js para leer asincrónicamente un archivo:
const fs = require ('fs'); fs.readFile ('/ ruta / a / archivo', (err, file) => { if (err) { // maneja el error } sino { // haz algo con el archivo } });
El
fs.readFile
método toma la ruta a un archivo y una devolución de llamada como argumentos. Cuando se lee el archivo, la devolución de llamada se invoca con un error como su primer argumento en caso de que algo salga mal, o null
como su primer argumento y el contenido del archivo como el segundo argumento en caso de éxito.
Sería bueno si pudiéramos usar este método como una promesa como esta:
fs.readFile ('/ ruta / a / archivo')
.then (archivo => {
// hacer algo con el archivo
})
.catch (err => {
// manejar el error
});
Para lograr esto, podemos envolverlo fácilmente en una Promesa:
const readFileAsync = ruta => { return new Promise ((resolver, rechazar) => { fs.readFile (ruta, (err, archivo) => { return err? rechazar (err): resolver (archivo); }); } ); }; // uso readFileAsync ('/ ruta / a / archivo') .then (archivo => { // hacer algo con el archivo }) .catch (err => { // manejar el error });
La
readFileAsync
función toma una ruta como argumento y devuelve una nueva Promesa. El constructor Promise toma una llamada función ejecutora que a su vez recibe las devoluciones de llamada resolve
y reject
.
Estas devoluciones de llamada son las que deben invocarse en caso de éxito y fracaso respectivamente. Cuando la devolución de llamada del
fs.readFile
método envuelto recibe un error, se pasará a reject
. Cuando file
tenga éxito, lo recibido se pasará a resolve
.Si ha prestado mucha atención, habrá notado que las promesas se basan realmente en devoluciones de llamadas .
Así es como puede convertir cualquier función basada en devolución de llamada en una función basada en Promesa.
Consejo adicional: para Node.js probablemente usarías
util.promisify
.
Puede hacer lo mismo con el código basado en eventos. Por ejemplo con
FileReader
. Digamos que desea leer un archivo en el navegador y convertirlo en un ArrayBuffer:const toArrayBuffer = blob => { const reader = new FileReader (); reader.onload = e => { const buffer = e.target.result; }; reader.onerror = e => { // manejar el error }; reader.readAsArrayBuffer (blob); };
También podemos convertir este código basado en eventos en Promesas de la misma manera:
const toArrayBuffer = blob => { const reader = new FileReader (); return new Promise ((resolver, rechazar) => { reader.onload = e => resolve (e.target.result); reader.onerror = e => rechazar (e.target.error); reader.readAsArrayBuffer (blob) ; }); };
Aquí básicamente hicimos lo mismo con los eventos en lugar de una devolución de llamada.
Usando resultados intermedios en una cadena Promesa
Si trabaja con Promises por un tiempo, se encontrará con la situación en la que encadena Promesas y necesita usar resultados intermedios que están fuera del alcance de una devolución de llamada de Promise:
const db = openDatabase (); db.getUser (id) .then (usuario => db.getOrders (usuario)) .then ( pedidos => db.getProducts (pedidos [0])) .then (productos => { // ¡no se puede acceder a los pedidos aquí! } )
En este ejemplo, abrimos una conexión de base de datos y recuperamos un usuario por su cuenta
id
, obtenemos los pedidos para ese usuario y luego los productos que están en el primer pedido del usuario.
El problema aquí es que dentro de la devolución de llamada para la Promesa devuelta desde
db.getProducts()
la orders
variable no es accesible ya que solo se define en el alcance de la devolución de llamada para la Promesa anterior, devuelta desde db.getOrders()
.
La solución más simple para esto sería inicializar la
orders
variable en el ámbito externo para que esté disponible en todas partes:const db = openDatabase (); dejar ordenes; // inicializado en el ámbito externo db.getUser (id) .then (user => db.getOrders (user)) .then ( orders => db.getProducts (orders [0])) .then (products => { // ¡Se puede acceder a los pedidos aquí! })
Funciona, pero no es la solución más limpia, especialmente cuando tienes una cadena Promise compleja con muchas variables. Esto daría como resultado una larga lista de variables que deben inicializarse.
En su lugar, debe usar
async
y await
dado que este es uno de los principales casos de uso. Le da el poder de usar código asincrónico con sintaxis sincrónica, por lo que todas las variables comparten el mismo alcance:const db = openDatabase (); const getUserData = async id => { const user = await db.getUser (id); const orders = await db.getOrders (usuario); const products = await db.getProducts (pedidos [0]); devolución de productos; }; getUserData (123);
Dentro de
getUserData
todas las variables ahora comparten el mismo alcance y todas son accesibles en ese alcance, sin embargo, el código es completamente asíncrono. La llamada a getUserData
no bloqueará ningún código siguiente.
Este es un ejemplo donde
async
y await
realmente brilla.Puedes combinar async / await con Promises
¿Cómo obtendría los productos devueltos
getUserData
en el ejemplo anterior?
Como
getUserData
es una async
función que usarías await
dentro de otra async
función:const getProducts = async () => {
productos const = await getUserData (123);
};
Pero también podrías usar
.then
:getUserData (123)
.then (productos => {
// use productos
})
Esto funciona porque una
async
función siempre devuelve una promesa implícita .Si ha prestado mucha atención, habrá notado que enasync/await
realidad se basa en Promesas.
Lo que es bueno saber
Como siempre, el diablo está en los detalles y esto también es cierto para la programación asincrónica en JavaScript. A menudo he visto a los desarrolladores perder detalles esenciales de implementación durante las entrevistas de trabajo, lo que demuestra una comprensión deficiente de los conceptos.
Las devoluciones de llamada de promesa siempre devuelven una promesa
Cuando se encadenan Promesas, generalmente se devuelve una Promesa de todos
.then
en la cadena:db.getUser (id)
.then (usuario => db.getOrders (usuario) )
.then (pedidos => db.getProducts (pedidos [0]) )
.then (productos => db.getStats (productos) )
En el ejemplo anterior de una promesa se devuelve desde cada devolución de llamada en el interior
.then
, lo que significa que db.getOrders
, db.getProducts
y db.getStats
todos vuelven una promesa.
Pero cuando tiene una cadena compleja, tarde o temprano deberá devolver algo que no sea una Promesa:
db.getUser (id) .then (user => db.getOrders (user)) .then (orders => db.getProducts (orders [0])) .then (products => products.length) // <- ¡Uy, no es una promesa! .then (numberOfProducts => { db.saveStats (numberOfProducts); return db.getStats (productos); }) ...
Sin embargo, el código anterior se ejecutará perfectamente bien ya que cualquier valor de retorno de una devolución de llamada de Promise en el interior
.then
o se .catch
incluirá automáticamente en una Promesa.
Esto significa que puede devolver valores arbitrarios dentro de una cadena de Promesas.
.then puede tomar dos argumentos
Normalmente, simplemente pasaría un argumento a
.then
la devolución de llamada que debería llamarse cuando se resuelva la Promesa. La devolución de llamada a la que se debe llamar cuando se rechaza la promesa se transfiere a .catch
.
Pero en
.then
realidad puede tomar estas dos devoluciones de llamada, la primera para cuando la Promesa se resuelve (éxito) y la segunda para cuando la Promesa rechaza (error).
Entonces, en lugar de esto:
fetch ('http://some.domain.com')
.then (respuesta => console.log ('success'))
.catch (err => console.error ('error'))
También puedes hacer esto:
fetch ('http://some.domain.com')
.then (respuesta => console.log ('success'),
err => console.error ('error') )
Podrías, pero no deberías . Siempre.
Usted debe siempre utilizar
.catch
en una cadena promesa para detectar errores.
La diferencia entre
.catch
y la devolución de llamada de error como segundo argumento .then
es que si se produce un error en una devolución de llamada exitosa en cualquier parte de la cadena de Promise, será detectado por .catch
:fetch ('http://some.domain.com')
.then (response => response.json ())
.then (json => fetch (...))
.then (response => response.text () )
.then (text => ...)
.catch (err => console.error (err)) // cualquier error se detectará aquí
Si pasa una devolución de llamada tanto éxito y una respuesta de error para
.then
y se produce un error en la devolución de llamada de éxito, que no va a ser atrapado por la devolución de llamada de error:fetch ('http://some.domain.com')
.then (successCallback,
errorCallback ) // un error en successCallback NO irá aquí
No hagas esto, siempre úsalo
.catch
.Lo que quizás no sepas
La programación asincrónica puede, por supuesto, ser mucho más compleja y exigir escenarios más complejos. He pedido a los candidatos en entrevistas de trabajo que codifiquen un escenario bastante simple en el que una llamada API asincrónica se debe realizar condicionalmente.
Llamada API asincrónica condicional
Digamos que una llamada a la API se debe hacer solo cuando el usuario ha iniciado sesión y luego, después de eso, se debe ejecutar algún código para eliminar la sesión del usuario. Pero la eliminación de la sesión solo se puede hacer una vez finalizada la llamada a la API. Si el usuario no ha iniciado sesión, se omite la llamada a la API y la sesión se elimina inmediatamente.
Cuando el usuario inicia sesión, simplemente podemos esperar a que se resuelva la Promesa devuelta por la llamada a la API y luego eliminar la sesión dentro de la devolución de llamada pasada a
.then
:if (userIsLoggedIn) {
apiCall ()
.then (res => {
deleteSession ()
})
}
Si el usuario no ha iniciado sesión, omitimos la llamada API y vamos directamente a
deleteSession
. Pero como la llamada a la API es asíncrona, cualquier código posterior se ejecutará inmediatamente, por lo que debemos duplicar la llamada a deleteSession
:const checkLogin = () => {
if (userIsLoggedIn) {
apiCall ()
.then (res => {
deleteSession ()
})
}
else {
deleteSession ();
} };
Ahora tenemos dos llamadas a las
deleteSession
que no es bueno. Como apiCall
es asíncrono, no tenemos más remedio que ejecutar deleteSession
dentro .then
y también crear una Promesa para cuando el usuario no está conectado:const checkLogin = () => { if (userIsLoggedIn) { return apiCall (); } else { return Promise.resolve (verdadero); // funciona, pero esto no es bueno } }; checkLogin () .then (() => { deleteSession (); })
La llamada duplicada a
deleteSession
se ha ido, pero ahora tenemos que devolver una promesa inútil de checkLogin
cuando el usuario no ha iniciado sesión, solo para que el código se ejecute en el orden correcto.
Esta es la solución que he visto a menudo en el código de producción, pero definitivamente es un olor a código y debe evitarse.
Este ejemplo sigue siendo bastante simple, pero cuando el código se vuelve más complejo, puede convertirse en un código difícil de seguir y difícil de seguir.
La única forma de resolver esto de una manera limpia y concisa es usar
async
y await
:const checkLogin = asíncrono () => { si (userIsLoggedIn) { esperar apiCall (); } deleteSession (); };
Ahora cuando el usuario haya iniciado sesión
apiCall
se ejecutará. Como checkLogin
ahora es una async
función y apiCall
tiene el prefijo await
, ahora esperará hasta apiCall
que termine y luego se ejecutará deleteSession
.
Cuando el usuario no haya iniciado sesión,
apiCall
simplemente se omitirá y solo deleteSession
se ejecutará. No más códigos duplicados, no más promesas inútiles.
Ese es el poder de
async
y await
.Una promesa con un tiempo de espera
Otro escenario común es cuando se debe establecer un tiempo de espera en una (posiblemente) llamada de API de larga ejecución.
Digamos que llamas a una API que tiene que responder en 5 segundos. Si no es así, se debe devolver un mensaje para indicar que la llamada tardó demasiado y se agotó el tiempo de espera.
He visto a los desarrolladores luchar con esto mientras se enredan en múltiples espaguetis de Promise. La solución es bastante simple de implementar
Promise.race
.Promise.race
toma una matriz (u otra Iterable
) de Promesas y resuelve o rechaza tan pronto como una de las Promesas en la matriz resuelve o rechaza. Puede usar esto para pasar la Promesa a la que desea aplicar un tiempo de espera junto con otra Promesa que se resuelve / rechaza después de ese tiempo Promise.race
.
Entonces, si su Promesa se resuelve dentro del tiempo de espera especificado, todo está bien. Pero si no es así, la segunda Promesa se resolverá o rechazará después del tiempo de espera y señalará que hubo un tiempo de espera:
const apiCall = url => buscar (url); const timeout = time => { return new Promise ((resolver, rechazar) => { setTimeout (() => rechazar ('Promise timed out'), time); }); }; Promise.race ([apiCall, timeout]) .then (respuesta => { // la respuesta de apiCall fue exitosa }) .catch (err => { // manejar el tiempo de espera });
Sin embargo, Aviso que cualquier otro error que pudiera ocurrir enapiCall
será también ir al.catch
controlador, por lo que necesita para ser capaz de determinar si el error es el tiempo de espera o un error real.
La clave para comprender la programación asincrónica en JavaScript
Para comprender realmente la programación asincrónica, debe comprender los conceptos básicos, los fundamentos sobre los que se construyó.
Recuerda:
async / await se basa en promesasLas promesas se basan en devoluciones de llamadalas devoluciones de llamada son la base de la programación asincrónica en JavaScript
Demasiados desarrolladores que entrevisté solo tienen una comprensión vaga o superficial de cómo funciona más o menos, pero esto no es suficiente.
Solo si realmente comprende la base puede comprender realmente y, en última instancia, dominar la programación asincrónica en JavaScript.
0 Comentarios