Promesses JavaScript : un didacticiel avec des exemples
Publié: 2022-03-11Les promesses sont un sujet brûlant dans les cercles de développement JavaScript, et vous devriez certainement vous familiariser avec elles. Ils ne sont pas faciles à comprendre; cela peut prendre quelques tutoriels, des exemples et une bonne quantité de pratique pour les comprendre.
Mon objectif avec ce didacticiel est de vous aider à comprendre les promesses JavaScript et de vous inciter à vous entraîner davantage à les utiliser. J'expliquerai ce que sont les promesses, quels problèmes elles résolvent et comment elles fonctionnent. Chaque étape, décrite dans cet article, est accompagnée d'un exemple de code jsbin
pour vous aider à travailler et à utiliser comme base pour une exploration plus approfondie.
Qu'est-ce qu'une promesse JavaScript ?
Une promesse est une méthode qui finit par produire une valeur. Elle peut être considérée comme le pendant asynchrone d'une fonction getter. Son essence peut être expliquée comme suit :
promise.then(function(value) { // Do something with the 'value' });
Les promesses peuvent remplacer l'utilisation asynchrone des rappels et offrent plusieurs avantages par rapport à celles-ci. Ils commencent à gagner du terrain à mesure que de plus en plus de bibliothèques et de frameworks les adoptent comme leur principal moyen de gérer l'asynchronicité. Ember.js est un excellent exemple d'un tel framework.
Il existe plusieurs bibliothèques qui implémentent la spécification Promises/A+. Nous apprendrons le vocabulaire de base et travaillerons sur quelques exemples de promesses JavaScript pour introduire les concepts sous-jacents de manière pratique. J'utiliserai l'une des bibliothèques d'implémentation les plus populaires, rsvp.js, dans les exemples de code.
Préparez-vous, nous allons lancer beaucoup de dés !
Obtenir la bibliothèque rsvp.js
Les promesses, et donc rsvp.js, peuvent être utilisées à la fois côté serveur et côté client. Pour l'installer pour nodejs , allez dans votre dossier de projet et tapez :
npm install --save rsvp
Si vous travaillez sur le front-end et utilisez bower, c'est juste un
bower install -S rsvp
une façon.
Si vous voulez juste entrer dans le jeu, vous pouvez l'inclure via une simple balise de script (et avec jsbin
, vous pouvez l'ajouter via le menu déroulant "Ajouter une bibliothèque") :
<script src="//cdn.jsdelivr.net/rsvp/3.0.6/rsvp.js"></script>
Quelles sont les propriétés d'une promesse ?
Une promesse peut être dans l'un des trois états suivants : en attente , remplie ou rejetée . Une fois créée, la promesse est en attente. À partir de là, il peut passer à l'état accompli ou rejeté. Nous appelons cette transition la résolution de la promesse . L'état résolu d'une promesse est son état final, donc une fois qu'elle est remplie ou rejetée, elle y reste.
La façon de créer une promesse dans rsvp.js est via ce qu'on appelle un constructeur révélateur. Ce type de constructeur prend un seul paramètre de fonction et l'appelle immédiatement avec deux arguments, fulfill
et reject
, qui peuvent faire passer la promesse à l'état fulfilled
ou rejected
:
var promise = new RSVP.Promise(function(fulfill, reject) { (...) });
Ce modèle de promesses JavaScript est appelé un constructeur révélateur car l'argument de la fonction unique révèle ses capacités à la fonction constructeur, mais garantit que les consommateurs de la promesse ne peuvent pas manipuler son état.
Les consommateurs de la promesse peuvent réagir à ses changements d'état en ajoutant leur gestionnaire via la méthode then
. Il faut une fonction de traitement d'exécution et une fonction de gestionnaire de rejet, qui peuvent toutes deux être manquantes.
promise.then(onFulfilled, onRejected);
Selon le résultat du processus de résolution de la promesse, le onFulfilled
ou onRejected
est appelé de manière asynchrone .
Voyons un exemple qui montre dans quel ordre les choses sont exécutées :
function dieToss() { return Math.floor(Math.random() * 6) + 1; } console.log('1'); var promise = new RSVP.Promise(function(fulfill, reject) { var n = dieToss(); if (n === 6) { fulfill(n); } else { reject(n); } console.log('2'); }); promise.then(function(toss) { console.log('Yay, threw a ' + toss + '.'); }, function(toss) { console.log('Oh, noes, threw a ' + toss + '.'); }); console.log('3');
Cet extrait imprime une sortie semblable à celle-ci :
1 2 3 Oh, noes, threw a 4.
Ou, si nous avons de la chance, nous voyons :
1 2 3 Yay, threw a 6.
Ce tutoriel promet de démontrer deux choses.
Premièrement, que les gestionnaires que nous avons attachés à la promesse ont bien été appelés après l'exécution de tous les autres codes, de manière asynchrone.
Deuxièmement, que le gestionnaire d'exécution n'a été appelé que lorsque la promesse a été remplie, avec la valeur avec laquelle elle a été résolue (dans notre cas, le résultat du lancer de dés). Il en va de même pour le gestionnaire de rejet.
Enchaînement de promesses et ruissellement
La spécification exige que la fonction then
(les gestionnaires) renvoie également une promesse, ce qui permet d'enchaîner les promesses, ce qui donne un code qui semble presque synchrone :
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage)
Ici, signupPayingUser
renvoie une promesse et chaque fonction de la chaîne de promesses est appelée avec la valeur de retour du gestionnaire précédent une fois qu'elle est terminée. À toutes fins pratiques, cela sérialise les appels sans bloquer le thread d'exécution principal.
Pour voir comment chaque promesse est résolue avec la valeur de retour de l'élément précédent dans la chaîne, nous revenons au lancer de dés. Nous voulons lancer les dés au maximum trois fois, ou jusqu'à ce que les six premiers arrivent jsbin :
function dieToss() { return Math.floor(Math.random() * 6) + 1; } function tossASix() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; if (n === 6) { fulfill(n); } else { reject(n); } }); } function logAndTossAgain(toss) { console.log("Tossed a " + toss + ", need to try again."); return tossASix(); } function logSuccess(toss) { console.log("Yay, managed to toss a " + toss + "."); } function logFailure(toss) { console.log("Tossed a " + toss + ". Too bad, couldn't roll a six"); } tossASix() .then(null, logAndTossAgain) //Roll first time .then(null, logAndTossAgain) //Roll second time .then(logSuccess, logFailure); //Roll third and last time
Lorsque vous exécutez cet exemple de code de promesses, vous verrez quelque chose comme ceci sur la console :
Tossed a 2, need to try again. Tossed a 1, need to try again. Tossed a 4. Too bad, couldn't roll a six.
La promesse renvoyée par tossASix
est rejetée lorsque le tirage au sort n'est pas un six, donc le gestionnaire de rejet est appelé avec le tirage réel. logAndTossAgain
imprime ce résultat sur la console et renvoie une promesse qui représente un autre lancer de dés. Ce lancer, à son tour, est également rejeté et déconnecté par le prochain logAndTossAgain
.
Parfois, cependant, vous avez de la chance* et vous réussissez à obtenir un six :
Tossed a 4, need to try again. Yay, managed to toss a 6.
* Vous n'avez pas à avoir cette chance. Il y a environ 42 % de chances d'obtenir au moins un six si vous lancez trois dés.

Cet exemple nous apprend aussi quelque chose de plus. Vous voyez comment aucun autre lancer n'a été effectué après le premier lancer réussi d'un six ? Notez que tous les gestionnaires de réalisation (les premiers arguments dans les appels à then
) de la chaîne sont null
, à l'exception du dernier, logSuccess
. La spécification exige que si un gestionnaire (réalisation ou rejet) n'est pas une fonction, la promesse retournée doit être résolue (réalisée ou rejetée) avec la même valeur. Dans l'exemple de promesses ci-dessus, le gestionnaire de réalisation, null
, n'est pas une fonction et la valeur de la promesse a été remplie avec un 6. Ainsi, la promesse renvoyée par l'appel then
(le suivant dans la chaîne) va également être remplie avec 6 comme valeur.
Cela se répète jusqu'à ce qu'un gestionnaire de réalisation réel (qui est une fonction) soit présent, de sorte que la réalisation se répercute jusqu'à ce qu'elle soit gérée. Dans notre cas, cela se produit à la fin de la chaîne où il est joyeusement déconnecté sur la console.
Gestion des erreurs
La spécification Promises/A+ exige que si une promesse est rejetée ou qu'une erreur est renvoyée dans un gestionnaire de rejet, elle doit être traitée par un gestionnaire de rejet qui est « en aval » de la source.
L'utilisation de la technique de ruissellement ci-dessous offre un moyen propre de gérer les erreurs :
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage) .then(null, displayAndSendErrorReport)
Étant donné qu'un gestionnaire de rejet n'est ajouté qu'à la toute fin de la chaîne, si un gestionnaire d'exécution de la chaîne est rejeté ou génère une erreur, il se répercute jusqu'à ce qu'il tombe sur displayAndSendErrorReport
.
Revenons à nos dés bien-aimés et voyons cela en action. Supposons que nous voulions simplement lancer les dés de manière asynchrone et imprimer les résultats :
var tossTable = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; function toss() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; fulfill(n); }); } function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUppercase() + "."); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain);
Lorsque vous exécutez ceci, rien ne se passe. Rien n'est imprimé sur la console et aucune erreur n'est renvoyée, apparemment.
En réalité, une erreur est générée, nous ne la voyons tout simplement pas car il n'y a pas de gestionnaire de rejet dans la chaîne. Étant donné que le code des gestionnaires est exécuté de manière asynchrone, avec une nouvelle pile, il n'est même pas déconnecté de la console. Réparons ça :
function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUpperCase() + "."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain) .then(null, logErrorMessage);
L'exécution du code ci-dessus affiche l'erreur maintenant :
"Tossed a TWO." "Oops: Cannot read property 'toUpperCase' of undefined"
Nous avons oublié de retourner quelque chose de logAndTossAgain
et la deuxième promesse est remplie avec undefined
. Le gestionnaire d'exécution suivant explose alors en essayant d'appeler toUpperCase
à ce sujet. C'est une autre chose importante à retenir : retournez toujours quelque chose des gestionnaires, ou soyez prêt dans les gestionnaires suivants à ne rien passer.
Construire plus haut
Nous avons maintenant vu les bases des promesses JavaScript dans l'exemple de code de ce tutoriel. Un grand avantage de leur utilisation est qu'ils peuvent être composés de manière simple pour produire des promesses "composées" avec le comportement que nous souhaitons. La bibliothèque rsvp.js
fournit une poignée, et vous pouvez toujours créer les vôtres en utilisant les primitives et celles de niveau supérieur.
Pour le dernier exemple, le plus complexe, nous voyageons dans le monde du jeu de rôle AD&D et lançons des dés pour obtenir des scores de personnage. Ces scores sont obtenus en lançant trois dés pour chaque compétence du personnage.
Permettez-moi d'abord de coller le code ici, puis d'expliquer ce qui est nouveau :
function toss() { var n = Math.floor(Math.random() * 6) + 1; return new RSVP.resolve(n); // [1] } function threeDice() { var tosses = []; function add(x, y) { return x + y; } for (var i=0; i<3; i++) { tosses.push(toss()); } return RSVP.all(tosses).then(function(results) { // [2] return results.reduce(add); // [3] }); } function logResults(result) { console.log("Rolled " + result + " with three dice."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } threeDice() .then(logResults) .then(null, logErrorMessage);
Nous sommes familiers avec le toss
du dernier exemple de code. Cela crée simplement une promesse qui est toujours remplie avec le résultat de lancer un dé. J'ai utilisé RSVP.resolve
, une méthode pratique qui crée une telle promesse avec moins de cérémonie (voir [1] dans le code ci-dessus).
Dans threeDice
, j'ai créé 3 promesses qui représentent chacune un lancer de dés et je les ai finalement combinées avec RSVP.all
. RSVP.all
prend un tableau de promesses et est résolu avec un tableau de leurs valeurs résolues, une pour chaque promesse constitutive, tout en conservant leur ordre. Cela signifie que nous avons le résultat des lancers dans les results
(voir [2] dans le code ci-dessus), et nous retournons une promesse qui est remplie avec leur somme (voir [3] dans le code ci-dessus).
La résolution de la promesse résultante enregistre ensuite le nombre total :
"Rolled 11 with three dice"
Utiliser des promesses pour résoudre de vrais problèmes
Les promesses JavaScript sont utilisées pour résoudre des problèmes dans des applications qui sont beaucoup plus complexes que les lancers de dés asynchrones sans raison valable .
Si vous remplacez le lancement de trois dés par l'envoi de trois requêtes ajax pour séparer les points de terminaison et poursuivez lorsqu'ils sont tous retournés avec succès (ou si l'un d'entre eux a échoué), vous disposez déjà d'une application utile de promesses et de RSVP.all
.
Les promesses, lorsqu'elles sont utilisées correctement, produisent un code facile à lire, sur lequel il est plus facile de raisonner et donc plus facile à déboguer que les rappels. Il n'est pas nécessaire de mettre en place des conventions concernant, par exemple, la gestion des erreurs puisqu'elles font déjà partie de la spécification.
Nous avons à peine effleuré la surface de ce que les promesses peuvent faire dans ce didacticiel JavaScript. Les bibliothèques Promise fournissent une bonne douzaine de méthodes et de constructeurs de bas niveau qui sont à votre disposition. Maîtrisez-les, et le ciel est la limite de ce que vous pouvez en faire.
A propos de l'auteur
Balint Erdi était un grand fan de jeux de rôle et d'AD&D il y a longtemps, et est maintenant une grande promesse et un fan d'Ember.js. Ce qui a été constant, c'est sa passion pour le rock & roll. C'est pourquoi il a décidé d'écrire un livre sur Ember.js qui utilise le rock & roll comme thème de l'application dans le livre. Inscrivez-vous ici pour savoir quand il sera lancé.