Conception asynchrone / attente JavaScript: forces, pièges et modèles d'utilisation

La construction async / wait est apparue dans la norme ES7. Il peut être considéré comme une amélioration remarquable dans le domaine de la programmation asynchrone en JavaScript. Il vous permet d'écrire du code qui ressemble à synchrone, mais est utilisé pour résoudre des tâches asynchrones et ne bloque pas le thread principal. Malgré le fait que async / attente est une grande nouvelle fonctionnalité du langage, son utilisation n'est pas si simple. Le matériel, dont nous publions la traduction aujourd'hui, est consacré à une étude approfondie de l'async / wait et à une histoire sur la façon d'utiliser correctement et efficacement ce mécanisme.

image

Points forts de l'async / attente


L'avantage le plus important pour un programmeur utilisant la construction async / wait est qu'il permet d'écrire du code asynchrone dans un style spécifique au code synchrone. Comparez le code écrit en utilisant async / wait avec le code basé sur les promesses.

// 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)); } 

Il est facile de remarquer que la version asynchrone / wait de l'exemple est plus compréhensible que sa version, dans laquelle la promesse est utilisée. Si vous ne faites pas attention au mot-clé await , ce code ressemblera à un ensemble régulier d'instructions exécutées de manière synchrone - comme dans JavaScript familier ou dans tout autre langage synchrone comme Python.

L'attractivité de async / wait n'est pas seulement due à une meilleure lisibilité du code. Ce mécanisme bénéficie en outre d'une excellente prise en charge du navigateur, qui ne nécessite aucune solution de contournement. Ainsi, aujourd'hui, les fonctions asynchrones prennent entièrement en charge tous les principaux navigateurs.


Tous les principaux navigateurs prennent en charge les fonctions asynchrones ( caniuse.com )

Ce niveau de support signifie, par exemple, que le code utilisant async / wait n'a pas besoin d'être transposé . De plus, il facilite le débogage, ce qui est peut-être encore plus important que l'absence de besoin de transpilation.

La figure suivante montre le processus de débogage d'une fonction asynchrone. Ici, lors de la définition d'un point d'arrêt sur la première instruction de la fonction et lors de l'exécution de la commande Step Over, lorsque le débogueur atteint la ligne où le mot clé await est utilisé, vous pouvez remarquer comment le débogueur s'arrête pendant un certain temps, en attendant que la fonction bookModel.fetchAll() se bookModel.fetchAll() , puis saute à la ligne où la commande .filter() est .filter() ! Un tel processus de débogage semble beaucoup plus simple que les promesses de débogage. Ici, lors du débogage d'un code similaire, vous devez définir un autre point d'arrêt dans la ligne .filter() .


Débogage d'une fonction asynchrone. Le débogueur attend la fin de la ligne d'attente et passe à la ligne suivante une fois l'opération terminée.

Une autre force du mécanisme considéré, moins évidente que ce que nous avons déjà examiné, est la présence du mot-clé async ici. Dans notre cas, son utilisation garantit que la valeur retournée par getBooksByAuthorWithAwait() est une promesse. Par conséquent, vous pouvez utiliser en toute sécurité la getBooksByAuthorWithAwait().then(...) ou await getBooksByAuthorWithAwait() construction await getBooksByAuthorWithAwait() dans le code qui appelle cette fonction. Prenons l'exemple suivant (notez que ce n'est pas recommandé):

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

Ici, la fonction getBooksByAuthorWithPromise() peut, si tout va bien, retourner une promesse ou, si quelque chose s'est mal passé - null . Par conséquent, si une erreur se produit, vous ne pouvez pas appeler .then() toute sécurité. Lors de la déclaration de fonctions à l'aide du async erreurs de ce type sont impossibles.

À propos de la perception erronée de l'async / wait


Dans certaines publications, la construction async / wait est comparée aux promesses et représenterait la prochaine génération de l'évolution de la programmation JavaScript asynchrone. Sur ce, avec tout le respect que je dois aux auteurs de ces publications, je me permets d'être en désaccord. L'asynchronisation / attente est une amélioration, mais ce n'est rien d'autre que du «sucre syntaxique», dont l'apparence ne conduit pas à un changement complet du style de programmation.

En substance, les fonctions asynchrones sont des promesses. Avant qu'un programmeur puisse utiliser correctement la construction async / wait, il doit bien étudier les promesses. De plus, dans la plupart des cas, en travaillant avec des fonctions asynchrones, vous devez utiliser des promesses.

Jetez un œil aux fonctions getBooksByAuthorWithAwait() et getBooksByAuthorWithPromises() de l'exemple ci-dessus. Veuillez noter qu'ils sont identiques non seulement en termes de fonctionnalité. Ils ont également exactement les mêmes interfaces.

Tout cela signifie que si vous appelez directement la fonction getBooksByAuthorWithAwait() , elle renverra la promesse.

En fait, l'essence du problème dont nous parlons ici est la perception incorrecte du nouveau design, lorsqu'il crée un sentiment trompeur qu'une fonction synchrone peut être convertie en asynchrone en raison de la simple utilisation de l' async et await mots clés et ne pas penser à autre chose.

Pièges de l'async / attente


Parlons des erreurs les plus courantes qui peuvent être commises en utilisant async / wait. En particulier, sur l'utilisation irrationnelle d'appels successifs de fonctions asynchrones.

Bien que le mot-clé await puisse donner au code un aspect synchrone, en l'utilisant, il convient de se rappeler que le code est asynchrone, ce qui signifie que vous devez faire très attention à l'appel séquentiel des fonctions asynchrones.

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

Ce code, en termes de logique, semble correct. Cependant, il y a un sérieux problème. Voilà comment ça marche.

  1. Les appels système await bookModel.fetchAll() et attendent la .fetchAll() commande .fetchAll() .
  2. Après avoir reçu le résultat de bookModel.fetchAll() await authorModel.fetch(authorId) sera appelé.

Notez que l'appel à authorModel.fetch(authorId) est indépendant des résultats de l'appel à bookModel.fetchAll() , et, en fait, ces deux commandes peuvent être exécutées en parallèle. Cependant, l'utilisation de await entraîne l'exécution séquentielle de ces deux appels. Le temps total d'exécution séquentielle de ces deux commandes sera plus long que le temps de leur exécution parallèle.

Voici l'approche correcte pour écrire un tel code:

 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), }; } 

Prenons un autre exemple de mauvaise utilisation des fonctions asynchrones. C'est encore pire que dans l'exemple précédent. Comme vous pouvez le voir, afin de charger de manière asynchrone une liste de certains éléments, nous devons compter sur les possibilités de promesses.

 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 bref, pour utiliser correctement les fonctions asynchrones, vous devez, comme à un moment où cela n'était pas possible, penser d'abord aux opérations asynchrones, puis écrire du code à l'aide de l' await . Dans les cas complexes, il sera probablement plus facile d'utiliser directement les promesses.

Gestion des erreurs


Lors de l'utilisation de promesses, l'exécution de code asynchrone peut se terminer soit comme prévu - puis ils disent que la promesse est résolue avec succès, ou avec une erreur - puis ils disent que la promesse est rejetée. Cela nous permet d'utiliser respectivement .then() et .catch() . Cependant, la gestion des erreurs à l'aide du mécanisme async / attente peut être délicate.

Construct construction try / catch


La façon standard de gérer les erreurs lors de l'utilisation de async / wait est avec la construction try / catch. Je recommande d'utiliser cette approche. Lors d'un appel en attente, la valeur renvoyée lorsque la promesse est rejetée est présentée comme exception. Voici un exemple:

 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 } } 

L'erreur interceptée dans le catch est exactement la valeur obtenue lorsque la promesse est rejetée. Après avoir intercepté une exception, nous pouvons appliquer plusieurs approches pour travailler avec elle:

  • Vous pouvez gérer l'exception et renvoyer la valeur normale. Si vous n'utilisez pas l'expression de return dans le catch pour renvoyer ce qui est attendu après l'exécution de la fonction asynchrone, cela équivaudra à utiliser la commande return undefined ;.
  • Vous pouvez simplement transmettre l'erreur à l'endroit où le code qui a échoué a été appelé et lui permettre d'y être traité. Vous pouvez lancer une erreur directement en utilisant une commande comme throw error; , qui vous permet d'utiliser la fonction async getBooksByAuthorWithAwait() dans la chaîne de promesses. Autrement dit, il peut être appelé à l'aide de la construction getBooksByAuthorWithAwait().then(...).catch(error => ...) . En outre, vous pouvez encapsuler l'erreur dans un objet Error , qui peut ressembler à throw new Error(error) . Cela permettra, par exemple, lors de la sortie des informations d'erreur sur la console, d'afficher la pile complète des appels.
  • L'erreur peut être représentée comme une promesse rejetée, elle ressemble à return Promise.reject(error) . Dans ce cas, cela équivaut à la commande throw error ; cette opération n'est pas recommandée.

Voici les avantages de l'utilisation de la construction try / catch:

  • De tels outils de gestion des erreurs existent en programmation depuis très longtemps, ils sont simples et compréhensibles. Disons que si vous avez de l'expérience en programmation dans d'autres langages, tels que C ++ ou Java, vous comprendrez facilement le périphérique try / catch en JavaScript.
  • Vous pouvez placer plusieurs appels en attente dans un bloc try / catch, ce qui vous permet de gérer toutes les erreurs au même endroit si vous n'avez pas besoin de gérer séparément les erreurs à chaque étape de l'exécution du code.

Il convient de noter qu'il existe un inconvénient dans le mécanisme try / catch. Étant donné que try / catch intercepte toutes les exceptions qui se produisent dans le bloc try , les exceptions qui ne sont pas liées aux promesses entreront également dans le gestionnaire de catch . Jetez un oeil à cet exemple.

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

Si vous exécutez ce code, vous verrez le message d'erreur ReferenceError: cb is not defined dans la console. Ce message est console.log() par la commande console.log() du catch , et non par JavaScript lui-même. Dans certains cas, de telles erreurs entraînent de graves conséquences. Par exemple, si vous appelez bookModel.fetchAll(); caché au fond d'une série d'appels de fonction et l'un des appels "avalera" une erreur, il sera très difficile de détecter une telle erreur.

▍ Fonction retour de deux valeurs


L'inspiration pour la prochaine façon de gérer les erreurs dans le code asynchrone est Go. Il permet aux fonctions asynchrones de renvoyer à la fois une erreur et un résultat. En savoir plus à ce sujet ici .

En résumé, les fonctions asynchrones, avec cette approche, peuvent être utilisées comme ceci:

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

Personnellement, je n'aime pas cela, car cette méthode de gestion des erreurs introduit le style de programmation Go dans JavaScript, ce qui ne semble pas naturel, bien que, dans certains cas, cela puisse être très utile.

▍Utilisation de .catch


La dernière façon de gérer les erreurs, dont nous parlerons, est d'utiliser .catch() .

Pensez à la façon dont l' await fonctionne. À savoir, l'utilisation de ce mot clé fait attendre le système jusqu'à ce que la promesse termine son travail. N'oubliez pas non plus qu'une commande de la forme promise.catch() renvoie également une promesse. Tout cela suggère que les erreurs de fonction asynchrones peuvent être traitées comme ceci:

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

Deux petits problèmes caractérisent cette approche:

  • Il s'agit d'un mélange de promesses et de fonctions asynchrones. Pour l'utiliser, il est nécessaire, comme dans d'autres cas similaires, de comprendre les caractéristiques du travail des promesses.
  • Cette approche n'est pas intuitive, car la gestion des erreurs est effectuée dans un endroit inhabituel.

Résumé


La construction async / attendent, qui a été introduite dans ES7, est certainement une amélioration des mécanismes de programmation asynchrone JavaScript. Cela peut faciliter la lecture et le débogage du code. Cependant, afin d'utiliser correctement async / wait, une compréhension approfondie des promesses est nécessaire, car async / wait n'est qu'un «sucre syntaxique» basé sur des promesses.

Nous espérons que ce matériel vous a permis de vous familiariser avec async / wait, et ce que vous avez appris ici vous évitera quelques erreurs courantes qui surviennent lors de l'utilisation de cette construction.

Chers lecteurs! Utilisez-vous la construction async / wait en JavaScript? Dans l'affirmative, veuillez nous indiquer comment vous gérez les erreurs dans le code asynchrone.

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


All Articles