Si vous débutez seulement en programmation, il y a de fortes chances que vous considériez les programmes comme un ensemble de blocs logiques séquentiels, où chaque bloc fait une chose spécifique et transmet son résultat afin que le bloc suivant puisse s'exécuter et ainsi de suite, et pour le vous avez en grande partie raison, la plupart des programmes s'exécutent de manière séquentielle, ce modèle nous permet de créer des programmes simples à écrire et à maintenir. Il existe cependant des cas d'utilisation spécifiques dans lesquels ce modèle séquentiel ne fonctionnerait pas ou ne serait pas optimal. À titre d'exemple, considérons une application de lecture de livres. Cette application possède quelques fonctionnalités avancées telles que la recherche de toutes les occurrences d'un mot, la navigation entre les signets, etc. Imaginez maintenant que l'utilisateur soit en train de lire un long livre et décide de rechercher toutes les occurrences d'un mot courant tel que « Le ». L'application prendra normalement quelques secondes pour rechercher et indexer toutes les occurrences de ce mot. Dans un programme séquentiel, l'utilisateur ne peut pas interagir avec l'application (changer de page ou surligner un texte) tant que l'opération de recherche n'est pas terminée. Espérons que vous puissiez voir que ce n’est pas une expérience utilisateur optimale !
Le diagramme illustre un flux d’exécution typique de l’application de lecture de livres. Si l'utilisateur lance une opération de longue durée (en l'occurrence la recherche de toutes les occurrences de « le » dans un gros livre), l'application « se fige » pendant toute la durée de cette opération. Dans ce cas, l'utilisateur continuera à cliquer sur le bouton de signet suivant sans résultat jusqu'à ce que l'opération de recherche soit terminée et toutes les opérations prendront effet en même temps, donnant à l'utilisateur final la sensation d'une application en retard.
Vous avez peut-être remarqué que cet exemple ne correspond pas vraiment au modèle séquentiel que nous avons présenté précédemment. En effet, les opérations ici sont indépendantes les unes des autres. L’utilisateur n’a pas besoin de connaître le nombre d’occurrences de « le » pour accéder au signet suivant, donc l’ordre d’exécution des opérations n’est pas vraiment important. Nous n’avons pas besoin d’attendre la fin de l’opération de recherche avant de pouvoir accéder au signet suivant. Une amélioration possible du flux d'exécution précédent repose sur cette logique : nous pouvons exécuter l'opération de recherche longue en arrière-plan, procéder à toutes les opérations entrantes, et une fois l'opération longue terminée, nous pouvons simplement en informer l'utilisateur. Le flux d'exécution devient le suivant :
Avec ce flux d’exécution, l’expérience utilisateur est considérablement améliorée. L'utilisateur peut désormais lancer une opération de longue durée, continuer à utiliser l'application normalement et être averti une fois l'opération terminée. C'est la base de la programmation asynchrone.
Javascript, entre autres langages, prend en charge ce style de programmation asynchrone en fournissant des API complètes pour réaliser à peu près tous les comportements asynchrones auxquels vous pouvez penser. En fin de compte, Javascript devrait être intrinsèquement un langage asynchrone. Si nous nous référons à l'exemple précédent, la logique asynchrone est à la base de toutes les applications d'interaction utilisateur, et Javascript a été principalement conçu pour être utilisé sur le navigateur où la plupart des programmes visent à répondre aux actions de l'utilisateur.
Ce qui suit vous donnera un bref guide sur le Javascript asynchrone :
Rappels
Dans un programme typique, vous trouverez généralement un certain nombre de fonctions. Pour utiliser une fonction, nous l'appelons avec un ensemble de paramètres. Le code de la fonction s'exécutera et renverra un résultat, rien d'extraordinaire. La programmation asynchrone modifie légèrement cette logique. Pour revenir à l'exemple de l'application de lecture de livres, nous ne pouvons pas utiliser une fonction régulière pour implémenter la logique de l'opération de recherche car l'opération prend un temps inconnu. Une fonction régulière reviendra essentiellement avant que l'opération ne soit terminée, et ce n'est pas le comportement auquel nous nous attendons. La solution est de spécifier une autre fonction qui sera exécutée une fois l'opération de recherche terminée. Cela modélise notre cas d'utilisation car notre programme peut continuer son déroulement normalement et une fois l'opération de recherche terminée, la fonction spécifiée s'exécutera pour informer l'utilisateur des résultats de la recherche. Cette fonction est ce que nous appelons une fonction de rappel :
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
Tout d’abord, nous définissons la fonction d’opération de recherche, searchOccurrences. Il faut le mot à rechercher et un deuxième paramètre « callback » qui sera la fonction à exécuter une fois l’opération de recherche effectuée. La fonction d'opération de recherche a été intentionnellement gardée abstraite, il suffit de se concentrer sur ses deux résultats possibles : le premier cas est celui où tout s'est bien passé et nous avons le résultat de la recherche dans la variable result. Dans ce cas, il suffit d'appeler la fonction de rappel avec les paramètres suivants : le premier paramètre est nul, ce qui signifie qu'aucune erreur ne s'est produite, le deuxième paramètre est le mot recherché et le troisième paramètre, peut-être le plus important des trois., est le résultat de l’opération de recherche.
Le deuxième cas est celui où une erreur se produit, c'est aussi un cas où l'exécution de l'opération de recherche est effectuée et il faut appeler la fonction de rappel. Nous utilisons un bloc try and catch pour intercepter toute erreur et nous appelons simplement la fonction de rappel avec l'objet d'erreur du bloc catch.
Nous avons ensuite défini la fonction de rappel, handleSearchOccurrences, nous avons gardé sa logique assez simple. Il s'agit simplement d'imprimer un message sur la console. Nous vérifions d’abord le paramètre « err » pour voir si une erreur s’est produite dans la fonction principale. Dans ce cas, nous informons simplement l'utilisateur que l'opération de recherche s'est terminée par une erreur. Si aucune erreur n'a été générée, nous imprimons un message avec le résultat de l'opération de recherche.
Enfin, nous appelons la fonction searchOccurrences avec le mot « le ». La fonction fonctionnera désormais normalement sans bloquer le programme principal et une fois la recherche terminée, le rappel sera exécuté et nous obtiendrons le message de résultat soit avec le résultat de la recherche, soit avec le message d'erreur.
Il est important de mentionner ici que nous n’avons accès qu’à la variable de résultat dans les fonctions main et de rappel. Si nous essayons quelque chose comme ceci :
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
le résultat de l'impression serait indéfini car les programmes n'attendent pas l'exécution de la fonction searchOccurrences. Il passe à l'instruction suivante qui est l'instruction d'impression avant que la variable de résultat ne soit affectée à l'intérieur de la fonction principale. En conséquence, nous imprimerons la variable de résultat non attribuée.
Donc, sur la base de cette logique, nous devrions conserver tout le code qui utilise la variable de résultat dans la fonction de rappel. Cela ne semble peut-être pas être un problème pour le moment, mais cela pourrait rapidement devenir un véritable problème. Imaginez le cas où nous avons une chaîne de fonctions asynchrones qui doivent s'exécuter en séquence. Dans la logique de rappel typique, vous implémenteriez quelque chose comme ceci :
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Gardez à l’esprit que chaque rappel a un paramètre d’erreur et que chaque erreur doit être traitée séparément. Cela rend le code déjà complexe ci-dessus encore plus complexe et difficile à maintenir. Espérons que les promesses soient là pour résoudre le problème de l’enfer des rappels, nous y reviendrons ensuite.
Promesses
Les promesses s'appuient sur les rappels et fonctionnent de la même manière. Ils ont été introduits dans le cadre des fonctionnalités ES6 pour résoudre quelques-uns des problèmes flagrants liés aux rappels tels que l'enfer des rappels. Les promesses fournissent leurs propres fonctions qui s'exécutent en cas de réussite (résolution) et lorsque des erreurs se produisent (rejet). Ce qui suit présente l'exemple searchOccurrences implémenté avec des promesses :
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Passons en revue les modifications que nous avons appliquées :
La fonction searchOccurrences renvoie une promesse. À l'intérieur de la promesse, nous gardons la même logique : nous avons deux fonctions de résolution et de rejet qui représentent nos rappels plutôt que d'avoir une seule fonction de rappel qui gère à la fois une exécution réussie et une exécution avec des erreurs. Les promesses séparent les deux résultats et fournissent une syntaxe claire lors de l'appel de la fonction principale. La fonction de résolution est « accrochée » à la fonction principale à l'aide du mot-clé « then ». Ici, nous spécifions simplement les deux paramètres de la fonction de résolution et imprimons le résultat de la recherche. Une chose similaire s'applique à la fonction de rejet, elle peut être accrochée à l'aide du mot-clé « catch ». J'espère que vous pourrez apprécier les avantages offerts par les promesses en termes de lisibilité et de propreté du code. Si vous en débattez encore, découvrez comment nous pouvons résoudre le problème de l'enfer des rappels en enchaînant les fonctions asynchrones pour qu'elles s'exécutent les unes après les autres :
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Async/Attendre
Async/Await sont le dernier ajout à notre ceinture d'outils asynchrone en Javascript. Introduits avec ES8, ils fournissent une nouvelle couche d'abstraction en plus des fonctions asynchrones en « attendant » simplement l'exécution d'une opération asynchrone. Le flux du programme se bloque au niveau de cette instruction jusqu'à ce qu'un résultat soit renvoyé par l'opération asynchrone, puis le programme passera à l'instruction suivante. Si vous pensez au flux d'exécution synchrone, vous avez raison. La boucle est bouclée ! Async/await tente d'apporter la simplicité de la programmation synchrone au monde asynchrone. Veuillez garder à l'esprit que cela n'est perçu que dans l'exécution et le code du programme. Tout reste pareil sous le capot, Async/await utilise toujours des promesses et les rappels sont leurs éléments de base.
Reprenons notre exemple et implémentons-le en utilisant Async/await :
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
Notre code n'a pas beaucoup changé, la chose importante à remarquer ici est le mot-clé « async » avant la déclaration de la fonction searchOccurrences. Cela indique que la fonction est asynchrone. Notez également le mot-clé « wait » lors de l’appel de la fonction searchOccurrences. Cela demandera au programme d'attendre l'exécution de la fonction jusqu'à ce que le résultat soit renvoyé avant que le programme puisse passer à l'instruction suivante, en d'autres termes, la variable de résultat contiendra toujours la valeur renvoyée par la fonction searchOccurrences et non la promesse de la fonction, en ce sens, Async/Await n'a pas d'état en attente comme Promises. Une fois l’exécution terminée, on passe à l’instruction print et cette fois le résultat contient effectivement le résultat de l’opération de recherche. Comme prévu, le nouveau code a le même comportement que s'il était synchrone.
Une autre chose mineure à garder à l'esprit est que puisque nous n'avons plus de fonctions de rappel, nous devons gérer l'erreur searchOccurrences dans la même fonction puisque nous ne pouvons pas simplement propager l'erreur à la fonction de rappel et la gérer là-bas. Ici, nous imprimons simplement un message d'erreur en cas d'erreur, à titre d'exemple.
Conclure
Dans cet article, nous avons passé en revue les différentes approches utilisées pour implémenter la logique asynchrone en Javascript. Nous avons commencé par explorer un exemple concret expliquant pourquoi nous devrions passer du style de programmation synchrone habituel au modèle asynchrone. Nous sommes ensuite passés aux rappels, qui sont les principaux éléments constitutifs du Javascript asynchrone. Les limitations des rappels nous ont conduit aux différentes alternatives qui ont été ajoutées au fil des années pour surmonter ces limitations, principalement les promesses et Async/await. La logique asynchrone peut être trouvée n'importe où sur le Web, que vous appeliez une API externe, que vous lanciez une requête de base de données, que vous écriviez sur le système de fichiers local ou même que vous attendiez la saisie d'un utilisateur sur un formulaire de connexion. Espérons que vous vous sentiez désormais plus en confiance pour résoudre ces problèmes en écrivant du Javascript asynchrone propre et maintenable !
Si vous aimez cet article, veuillez consulter le Blog CLA où nous discutons de divers sujets sur la façon de se lancer dans la technologie. Consultez également notre chaîne YouTube pour nos précédents ateliers gratuits et suivez-nous sur les réseaux sociaux pour ne rien manquer des prochains !
- Préparez votre carrière pour l'avenir en améliorant vos compétences en HTML, CSS et JavaScript avec le [Bootcamp de développement Web] de Code Labs Academy(/courses/web-development).*