JavaScript asynchrone : de l'enfer du rappel à l'asynchrone et à l'attente
Publié: 2022-03-11L'une des clés pour écrire une application Web réussie est de pouvoir effectuer des dizaines d'appels AJAX par page.
Il s'agit d'un défi de programmation asynchrone typique, et la façon dont vous choisissez de gérer les appels asynchrones fera, en grande partie, faire ou casser votre application, et par extension potentiellement toute votre startup.
La synchronisation des tâches asynchrones en JavaScript a longtemps été un problème sérieux.
Ce défi affecte autant les développeurs back-end utilisant Node.js que les développeurs front-end utilisant n'importe quel framework JavaScript. La programmation asynchrone fait partie de notre travail quotidien, mais le défi est souvent pris à la légère et n'est pas envisagé au bon moment.
Une brève histoire du JavaScript asynchrone
La première solution, et la plus simple, est venue sous la forme de fonctions imbriquées en tant que rappels . Cette solution a conduit à quelque chose appelé callback hell , et trop d'applications en ressentent encore la brûlure.
Ensuite, nous avons eu des Promesses . Ce modèle rendait le code beaucoup plus facile à lire, mais il était loin du principe Ne vous répétez pas (DRY). Il y avait encore trop de cas où il fallait répéter les mêmes morceaux de code pour bien gérer le flux de l'application. Le dernier ajout, sous la forme d'instructions async/wait, a finalement rendu le code asynchrone en JavaScript aussi facile à lire et à écrire que n'importe quel autre morceau de code.
Examinons les exemples de chacune de ces solutions et réfléchissons à l'évolution de la programmation asynchrone en JavaScript.
Pour ce faire, nous allons examiner une tâche simple qui effectue les étapes suivantes :
- Vérifiez le nom d'utilisateur et le mot de passe d'un utilisateur.
- Obtenez des rôles d'application pour l'utilisateur.
- Enregistrez le temps d'accès à l'application pour l'utilisateur.
Approche 1 : Callback Hell (« La pyramide du destin »)
L'ancienne solution pour synchroniser ces appels était via des rappels imbriqués. Il s'agissait d'une approche décente pour les tâches JavaScript asynchrones simples, mais elle ne serait pas mise à l'échelle en raison d'un problème appelé callback hell.
Le code des trois tâches simples ressemblerait à ceci :
const verifyUser = function(username, password, callback){ dataBase.verifyUser(username, password, (error, userInfo) => { if (error) { callback(error) }else{ dataBase.getRoles(username, (error, roles) => { if (error){ callback(error) }else { dataBase.logAccess(username, (error) => { if (error){ callback(error); }else{ callback(null, userInfo, roles); } }) } }) } }) };
Chaque fonction reçoit un argument qui est une autre fonction appelée avec un paramètre qui est la réponse de l'action précédente.
Trop de gens connaîtront un gel du cerveau simplement en lisant la phrase ci-dessus. Avoir une application avec des centaines de blocs de code similaires causera encore plus de problèmes à la personne qui maintient le code, même si elle l'a écrit elle-même.
Cet exemple devient encore plus compliqué une fois que vous réalisez qu'un database.getRoles
est une autre fonction qui a des rappels imbriqués.
const getRoles = function (username, callback){ database.connect((connection) => { connection.query('get roles sql', (result) => { callback(null, result); }) }); };
En plus d'avoir du code difficile à maintenir, le principe DRY n'a absolument aucune valeur dans ce cas. La gestion des erreurs, par exemple, est répétée dans chaque fonction et le rappel principal est appelé à partir de chaque fonction imbriquée.
Les opérations JavaScript asynchrones plus complexes, telles que la boucle via des appels asynchrones, constituent un défi encore plus important. En fait, il n'y a pas de moyen trivial de faire cela avec des rappels. C'est pourquoi les bibliothèques JavaScript Promise comme Bluebird et Q ont eu autant de succès. Ils fournissent un moyen d'effectuer des opérations courantes sur des requêtes asynchrones que le langage lui-même ne fournit pas déjà.
C'est là qu'interviennent les promesses JavaScript natives.
Promesses JavaScript
Les promesses étaient la prochaine étape logique pour échapper à l'enfer des rappels. Cette méthode n'a pas supprimé l'utilisation des rappels, mais elle a simplifié l'enchaînement des fonctions et simplifié le code, le rendant beaucoup plus facile à lire.

Avec Promises en place, le code de notre exemple JavaScript asynchrone ressemblerait à ceci :
const verifyUser = function(username, password) { database.verifyUser(username, password) .then(userInfo => dataBase.getRoles(userInfo)) .then(rolesInfo => dataBase.logAccess(rolesInfo)) .then(finalResult => { //do whatever the 'callback' would do }) .catch((err) => { //do whatever the error handler needs }); };
Pour atteindre ce genre de simplicité, toutes les fonctions utilisées dans l'exemple devraient être Promisified . Voyons comment la méthode getRoles
serait mise à jour pour renvoyer une Promise
:
const getRoles = function (username){ return new Promise((resolve, reject) => { database.connect((connection) => { connection.query('get roles sql', (result) => { resolve(result); }) }); }); };
Nous avons modifié la méthode pour renvoyer une Promise
, avec deux rappels, et la Promise
elle-même effectue des actions à partir de la méthode. Désormais, les rappels de resolve
et de reject
seront mappés aux méthodes Promise.then
et Promise.catch
respectivement.
Vous remarquerez peut-être que la méthode getRoles
est toujours sujette au phénomène de la pyramide du destin. Cela est dû à la façon dont les méthodes de base de données sont créées car elles ne renvoient pas Promise
. Si nos méthodes d'accès à la base de données renvoyaient également Promise
, la méthode getRoles
ressemblerait à ceci :
const getRoles = new function (userInfo) { return new Promise((resolve, reject) => { database.connect() .then((connection) => connection.query('get roles sql')) .then((result) => resolve(result)) .catch(reject) }); };
Approche 3 : Asynchrone/Attente
La pyramide du destin a été considérablement atténuée avec l'introduction de Promises. Cependant, nous devions toujours compter sur les rappels qui sont transmis aux méthodes .then
et .catch
d'un Promise
.
Les promesses ont ouvert la voie à l'une des améliorations les plus intéressantes de JavaScript. ECMAScript 2017 a introduit du sucre syntaxique en plus des promesses en JavaScript sous la forme d'instructions async
et en await
.
Ils nous permettent d'écrire du code basé sur Promise
comme s'il était synchrone, mais sans bloquer le thread principal, comme le montre cet exemple de code :
const verifyUser = async function(username, password){ try { const userInfo = await dataBase.verifyUser(username, password); const rolesInfo = await dataBase.getRoles(userInfo); const logStatus = await dataBase.logAccess(userInfo); return userInfo; }catch (e){ //handle errors as needed } };
L'attente de la Promise
de résolution n'est autorisée que dans les fonctions async
, ce qui signifie que verifyUser
doit être défini à l'aide de async function
.
Cependant, une fois ce petit changement effectué, vous pouvez await
n'importe quelle Promise
sans modifications supplémentaires dans d'autres méthodes.
Async - Une résolution tant attendue d'une promesse
Les fonctions asynchrones sont la prochaine étape logique dans l'évolution de la programmation asynchrone en JavaScript. Ils rendront votre code beaucoup plus propre et plus facile à entretenir. Déclarer une fonction comme async
garantira qu'elle renvoie toujours une Promise
afin que vous n'ayez plus à vous en soucier.
Pourquoi devriez-vous commencer à utiliser la fonction async
JavaScript dès aujourd'hui ?
- Le code résultant est beaucoup plus propre.
- La gestion des erreurs est beaucoup plus simple et repose sur
try
/catch
comme dans tout autre code synchrone. - Le débogage est beaucoup plus simple. La définition d'un point d'arrêt à l'intérieur d'un bloc
.then
ne passera pas au.then
suivant car il ne fait que parcourir le code synchrone. Mais, vous pouvez parcourir les appels d'await
comme s'il s'agissait d'appels synchrones.