Diseño asincrónico / en espera de JavaScript: fortalezas, dificultades y patrones de uso

La construcción asíncrona / espera apareció en el estándar ES7. Se puede considerar una mejora notable en el campo de la programación asincrónica en JavaScript. Le permite escribir código que parece síncrono, pero se utiliza para resolver tareas asíncronas y no bloquea el hilo principal. A pesar de que async / await es una gran característica nueva del lenguaje, usarlo correctamente no es tan simple. El material, cuya traducción publicamos hoy, está dedicado a un estudio exhaustivo de async / wait y una historia sobre cómo usar este mecanismo de manera correcta y efectiva.

imagen

Fortalezas de asíncrono / espera


El beneficio más importante que obtiene un programador que utiliza la construcción async / await es que permite escribir código asincrónico en un estilo específico para el código síncrono. Compare el código escrito usando async / await con el código basado en promesas.

// async/await async getBooksByAuthorWithAwait(authorId) {  const books = await bookModel.fetchAll();  return books.filter(b => b.authorId === authorId); } //  getBooksByAuthorWithPromise(authorId) {  return bookModel.fetchAll()    .then(books => books.filter(b => b.authorId === authorId)); } 

Es fácil notar que la versión asíncrona / en espera del ejemplo es más comprensible que su versión, en la que se utiliza la promesa. Si no presta atención a la palabra clave en await , este código se verá como un conjunto regular de instrucciones ejecutadas sincrónicamente, como en JavaScript familiar o en cualquier otro lenguaje sincrónico como Python.

El atractivo de async / await no solo se debe a la legibilidad mejorada del código. Este mecanismo, además, goza de una excelente compatibilidad con el navegador, que no requiere soluciones alternativas. Por lo tanto, hoy las funciones asincrónicas son totalmente compatibles con todos los principales navegadores.


Todos los principales navegadores admiten funciones asincrónicas ( caniuse.com )

Este nivel de soporte significa, por ejemplo, que el código que usa async / await no necesita ser transpuesto . Además, facilita la depuración, que quizás sea aún más importante que la falta de necesidad de transpilación.

La siguiente figura muestra el proceso de depuración de una función asincrónica. Aquí, al establecer un punto de interrupción en la primera instrucción de la función y al ejecutar el comando Paso a paso, cuando el depurador llega a la línea donde se usa la palabra clave de await , puede observar cómo el depurador hace una pausa durante un tiempo, esperando que bookModel.fetchAll() función bookModel.fetchAll() , y luego salta a la línea donde se .filter() comando .filter() ! Tal proceso de depuración parece mucho más simple que las promesas de depuración. Aquí, al depurar un código similar, debería establecer otro punto de interrupción en la .filter() .


Depuración de una función asincrónica. El depurador esperará a que se complete la línea de espera y pasará a la siguiente línea después de que se complete la operación

Otra fortaleza del mecanismo en consideración, que es menos obvio que lo que ya hemos examinado, es la presencia de la palabra clave async aquí. En nuestro caso, su uso garantiza que el valor devuelto por getBooksByAuthorWithAwait() sea ​​una promesa. Como resultado, puede usar de forma segura el getBooksByAuthorWithAwait().then(...) o await getBooksByAuthorWithAwait() construcción await getBooksByAuthorWithAwait() en el código que llama a esta función. Considere el siguiente ejemplo (tenga en cuenta que esto no se recomienda):

 getBooksByAuthorWithPromise(authorId) { if (!authorId) {   return null; } return bookModel.fetchAll()   .then(books => books.filter(b => b.authorId === authorId)); } } 

Aquí la función getBooksByAuthorWithPromise() puede, si todo está bien, devolver una promesa o, si algo salió mal, null . Como resultado, si se produce un error, no puede llamar con seguridad .then() . Al declarar funciones utilizando la async errores de este tipo son imposibles.

Sobre la percepción errónea de async / wait


En algunas publicaciones, la construcción asíncrona / en espera se compara con las promesas y se dice que representa la próxima generación de la evolución de la programación asíncrona de JavaScript. Con esto, con el debido respeto a los autores de tales publicaciones, me permito estar en desacuerdo. Async / await es una mejora, pero no es más que "azúcar sintáctico", cuya apariencia no conduce a un cambio completo en el estilo de programación.

En esencia, las funciones asincrónicas son promesas. Antes de que un programador pueda usar adecuadamente la construcción asíncrona / espera, debe estudiar bien las promesas. Además, en la mayoría de los casos, al trabajar con funciones asincrónicas, debe usar promesas.

Eche un vistazo a las getBooksByAuthorWithAwait() y getBooksByAuthorWithPromises() del ejemplo anterior. Tenga en cuenta que son idénticos no solo en términos de funcionalidad. También tienen exactamente las mismas interfaces.

Todo esto significa que si llama directamente a la función getBooksByAuthorWithAwait() , devolverá la promesa.

De hecho, la esencia del problema del que estamos hablando aquí es la percepción incorrecta del nuevo diseño, cuando crea una sensación engañosa de que una función síncrona se puede convertir en asíncrona debido al simple uso del async y await palabras clave y no pensar en otra cosa.

Las trampas de async / aguardan


Hablemos de los errores más comunes que se pueden cometer usando async / await. En particular, sobre el uso irracional de llamadas sucesivas de funciones asincrónicas.

Aunque la palabra clave await puede hacer que el código parezca síncrono, al usarlo, vale la pena recordar que el código es asíncrono, lo que significa que debe tener mucho cuidado con las llamadas en serie a funciones asíncronas.

 async getBooksAndAuthor(authorId) { const books = await bookModel.fetchAll(); const author = await authorModel.fetch(authorId); return {   author,   books: books.filter(book => book.authorId === authorId), }; } 

Este código, en términos de lógica, parece correcto. Sin embargo, hay un problema grave. Así es como funciona.

  1. Las llamadas del sistema await bookModel.fetchAll() y espera a que se .fetchAll() comando .fetchAll() .
  2. Después de recibir el resultado de bookModel.fetchAll() await authorModel.fetch(authorId) .

Tenga en cuenta que la llamada a authorModel.fetch(authorId) es independiente de los resultados de la llamada a bookModel.fetchAll() y, de hecho, estos dos comandos se pueden ejecutar en paralelo. Sin embargo, el uso de los resultados de await en estas dos llamadas se ejecutan secuencialmente. El tiempo total de ejecución secuencial de estos dos comandos será más largo que el tiempo de su ejecución paralela.

Aquí está el enfoque correcto para escribir dicho código:

 async getBooksAndAuthor(authorId) { const bookPromise = bookModel.fetchAll(); const authorPromise = authorModel.fetch(authorId); const book = await bookPromise; const author = await authorPromise; return {   author,   books: books.filter(book => book.authorId === authorId), }; } 

Considere otro ejemplo del mal uso de las funciones asincrónicas. Esto es aún peor que en el ejemplo anterior. Como puede ver, para cargar asincrónicamente una lista de ciertos elementos, debemos confiar en las posibilidades de las promesas.

 async getAuthors(authorIds) { //  ,     // const authors = _.map( //   authorIds, //   id => await authorModel.fetch(id)); //   const promises = _.map(authorIds, id => authorModel.fetch(id)); const authors = await Promise.all(promises); } 

En pocas palabras, entonces, para usar correctamente las funciones asincrónicas, necesita, como en un momento en que esto no era posible, primero pensar en las operaciones asincrónicas y luego escribir el código usando await . En casos complejos, probablemente será más fácil usar las promesas directamente.

Manejo de errores


Cuando se usan promesas, la ejecución del código asincrónico puede finalizar como se esperaba, luego dicen que la promesa se resolvió con éxito o con un error, y luego dicen que la promesa se rechaza. Esto nos permite usar .then() y .catch() , respectivamente. Sin embargo, el manejo de errores utilizando el mecanismo asíncrono / espera puede ser complicado.

▍ tratar / atrapar construcción


La forma estándar de manejar errores cuando se usa async / await es con la construcción try / catch. Recomiendo usar este enfoque. Al realizar una llamada en espera, el valor devuelto cuando se rechaza la promesa se presenta como una excepción. Aquí hay un ejemplo:

 class BookModel { fetchAll() {   return new Promise((resolve, reject) => {     window.setTimeout(() => { reject({'error': 400}) }, 1000);   }); } } // async/await async getBooksByAuthorWithAwait(authorId) { try { const books = await bookModel.fetchAll(); } catch (error) { console.log(error);    // { "error": 400 } } 

El error detectado en el catch es exactamente el valor obtenido cuando se rechaza la promesa. Después de detectar una excepción, podemos aplicar varios enfoques para trabajar con ella:

  • Puede manejar la excepción y devolver el valor normal. Si no utiliza la expresión de return en el catch para devolver lo que se espera después de que se ejecute la función asincrónica, esto será equivalente a usar el comando return undefined ;
  • Simplemente puede pasar el error al lugar donde se llamó el código que falló y permitir que se procese allí. Puede lanzar un error directamente usando un comando como throw error; , que le permite utilizar la función async getBooksByAuthorWithAwait() en la cadena de promesas. Es decir, se puede getBooksByAuthorWithAwait().then(...).catch(error => ...) usando la getBooksByAuthorWithAwait().then(...).catch(error => ...) . Además, puede ajustar el error en un objeto Error , que puede parecer throw new Error(error) . Esto permitirá, por ejemplo, al enviar información de error a la consola, ver la pila completa de llamadas.
  • El error se puede representar como una promesa rechazada, parece que return Promise.reject(error) . En este caso, esto es equivalente al comando throw error ; no se recomienda hacerlo.

Estos son los beneficios de usar la construcción try / catch:

  • Tales herramientas de manejo de errores han existido en la programación durante mucho tiempo, son simples y comprensibles. Supongamos que si tiene experiencia en programación en otros lenguajes, como C ++ o Java, comprenderá fácilmente el dispositivo try / catch en JavaScript.
  • Puede colocar varias llamadas en espera en un bloque try / catch, lo que le permite manejar todos los errores en un solo lugar si no necesita manejar por separado los errores en cada paso de la ejecución del código.

Cabe señalar que hay un inconveniente en el mecanismo try / catch. Dado que try / catch captura cualquier excepción que ocurra en el bloque try , esas excepciones que no están relacionadas con las promesas también entrarán en el controlador catch . Echa un vistazo a este ejemplo.

 class BookModel { fetchAll() {   cb();    //    ,   `cb`  ,       return fetch('/books'); } } try { bookModel.fetchAll(); } catch(error) { console.log(error);  //       "cb is not defined" } 

Si ejecuta este código, verá el error ReferenceError: cb is not defined en la consola. Este mensaje es emitido por el comando console.log() desde el catch , y no por el propio JavaScript. En algunos casos, tales errores conducen a graves consecuencias. Por ejemplo, si llama a bookModel.fetchAll(); oculto en una serie de llamadas a funciones y una de las llamadas "tragará" un error, será muy difícil detectarlo.

▍ Función de retorno de dos valores


La inspiración para la siguiente forma de manejar errores en código asincrónico es Go. Permite que las funciones asincrónicas devuelvan tanto un error como un resultado. Lea más sobre esto aquí .

En pocas palabras, las funciones asincrónicas, con este enfoque, se pueden usar así:

 [err, user] = await to(UserModel.findById(1)); 

Personalmente, no me gusta esto, porque este método de manejo de errores introduce el estilo de programación Go en JavaScript, que parece poco natural, aunque, en algunos casos, puede ser muy útil.

▍Uso de .catch


La última forma de manejar los errores, de los que hablaremos, es usar .catch() .

Piensa en cómo funciona la await . Es decir, el uso de esta palabra clave hace que el sistema espere hasta que la promesa complete su trabajo. Además, recuerde que un comando de la forma promise.catch() también devuelve una promesa. Todo esto sugiere que los errores de función asincrónica se pueden manejar de esta manera:

 // books   undefined   , //    catch     let books = await bookModel.fetchAll() .catch((error) => { console.log(error); }); 

Dos pequeños problemas son característicos de este enfoque:

  • Esta es una mezcla de promesas y funciones asincrónicas. Para usar esto, es necesario, como en otros casos similares, comprender las características del trabajo de las promesas.
  • Este enfoque no es intuitivo, ya que el manejo de errores se realiza en un lugar inusual.

Resumen


La construcción async / await, que se introdujo en ES7, es definitivamente una mejora en los mecanismos de programación asincrónica de JavaScript. Puede facilitar la lectura y la depuración de código. Sin embargo, para usar async / await correctamente, es necesario un conocimiento profundo de las promesas, ya que async / await es solo "azúcar sintáctico" basado en promesas.

Esperamos que este material le haya permitido familiarizarse más con async / wait, y lo que aprendió aquí lo salvará de algunos errores comunes que surgen al usar esta construcción.

Estimados lectores! ¿Utiliza la construcción async / await en JavaScript? Si es así, díganos cómo maneja los errores en el código asíncrono.

Source: https://habr.com/ru/post/es414373/


All Articles