Revenons au problème évoqué dans le chapitre Introduction : les callbacks : nous avons une séquence de tâches asynchrones à effectuer les unes après les autres — par exemple, charger des scripts. Comment bien le coder ?
Les promesses fournissent quelques recettes pour y parvenir.
Dans ce chapitre, nous couvrons l’enchaînement des promesses.
Cela ressemble à ceci :
nouvelle promesse (fonction (résoudre, rejeter) { setTimeout(() => résoudre(1), 1000); // (*) }).then(function(result) { // (**) alerte (résultat); // 1 renvoyer le résultat * 2 ; }).then(function(result) { // (***) alerte (résultat); // 2 renvoyer le résultat * 2 ; }).then(fonction(résultat) { alerte (résultat); // 4 renvoyer le résultat * 2 ; });
L'idée est que le résultat passe par la chaîne de gestionnaires .then
.
Voici le flux :
La promesse initiale se résout en 1 seconde (*)
,
Ensuite, le gestionnaire .then
est appelé (**)
, ce qui crée à son tour une nouvelle promesse (résolue avec 2
valeurs).
Le suivant then
(***)
obtient le résultat du précédent, le traite (double) et le transmet au gestionnaire suivant.
…et ainsi de suite.
Au fur et à mesure que le résultat est transmis le long de la chaîne de gestionnaires, nous pouvons voir une séquence d'appels alert
: 1
→ 2
→ 4
.
Le tout fonctionne, car chaque appel à un .then
renvoie une nouvelle promesse, afin que nous puissions appeler le prochain .then
dessus.
Lorsqu'un gestionnaire renvoie une valeur, celle-ci devient le résultat de cette promesse, donc le prochain .then
est appelé avec elle.
Une erreur classique du débutant : techniquement, nous pouvons également ajouter plusieurs .then
à une seule promesse. Ce n’est pas un enchaînement.
Par exemple:
let promise = new Promise (fonction (résolution, rejet) { setTimeout(() => résoudre(1), 1000); }); promesse.then(fonction(résultat) { alerte (résultat); // 1 renvoyer le résultat * 2 ; }); promesse.then(fonction(résultat) { alerte (résultat); // 1 renvoyer le résultat * 2 ; }); promesse.then(fonction(résultat) { alerte (résultat); // 1 renvoyer le résultat * 2 ; });
Ce que nous avons fait ici, c'est simplement ajouter plusieurs gestionnaires à une seule promesse. Ils ne se transmettent pas le résultat ; au lieu de cela, ils le traitent de manière indépendante.
Voici l'image (comparez-la avec le chaînage ci-dessus) :
Tous .then
sur la même promesse, obtiennent le même résultat – le résultat de cette promesse. Ainsi, dans le code ci-dessus, l' alert
affiche la même chose : 1
.
En pratique, nous avons rarement besoin de plusieurs gestionnaires pour une seule promesse. Le chaînage est utilisé beaucoup plus souvent.
Un gestionnaire, utilisé dans .then(handler)
peut créer et renvoyer une promesse.
Dans ce cas, d'autres gestionnaires attendent que le problème se stabilise, puis obtiennent son résultat.
Par exemple:
nouvelle promesse (fonction (résoudre, rejeter) { setTimeout(() => résoudre(1), 1000); }).then(fonction(résultat) { alerte (résultat); // 1 retourner une nouvelle promesse ((résoudre, rejeter) => { // (*) setTimeout(() => résoudre(résultat * 2), 1000); }); }).then(function(result) { // (**) alerte (résultat); // 2 renvoyer une nouvelle promesse ((résoudre, rejeter) => { setTimeout(() => résoudre(résultat * 2), 1000); }); }).then(fonction(résultat) { alerte (résultat); // 4 });
Ici, le premier .then
affiche 1
et renvoie new Promise(…)
dans la ligne (*)
. Après une seconde, il est résolu et le résultat (l'argument de resolve
, ici c'est result * 2
) est transmis au gestionnaire du second .then
. Ce gestionnaire est dans la ligne (**)
, il affiche 2
et fait la même chose.
Le résultat est donc le même que dans l'exemple précédent : 1 → 2 → 4, mais maintenant avec un délai d'une seconde entre les appels alert
.
Renvoyer des promesses nous permet de construire des chaînes d'actions asynchrones.
Utilisons cette fonctionnalité avec le promis loadScript
, défini dans le chapitre précédent, pour charger les scripts un par un, dans l'ordre :
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(fonction(script) { return loadScript("https://javascript.info/article/promise-chaining/two.js"); }) .then(fonction(script) { return loadScript("https://javascript.info/article/promise-chaining/trois.js"); }) .then(fonction(script) { // utilise les fonctions déclarées dans les scripts // pour montrer qu'ils ont bien chargé un(); deux(); trois(); });
Ce code peut être un peu plus court avec les fonctions fléchées :
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(script => loadScript("https://javascript.info/article/promise-chaining/two.js")) .then(script => loadScript("https://javascript.info/article/promise-chaining/trois.js")) .puis(script => { // les scripts sont chargés, on peut utiliser les fonctions qui y sont déclarées un(); deux(); trois(); });
Ici, chaque appel loadScript
renvoie une promesse, et le prochain .then
s'exécute lorsqu'il est résolu. Ensuite, il lance le chargement du script suivant. Les scripts sont donc chargés les uns après les autres.
Nous pouvons ajouter des actions plus asynchrones à la chaîne. Veuillez noter que le code est toujours « plat » : il grandit vers le bas, pas vers la droite. Il n’y a aucun signe de la « pyramide du malheur ».
Techniquement, nous pourrions ajouter .then
directement à chaque loadScript
, comme ceci :
loadScript("https://javascript.info/article/promise-chaining/one.js").then(script1 => { loadScript("https://javascript.info/article/promise-chaining/two.js").then(script2 => { loadScript("https://javascript.info/article/promise-chaining/trois.js").then(script3 => { // cette fonction a accès aux variables script1, script2 et script3 un(); deux(); trois(); }); }); });
Ce code fait la même chose : charge 3 scripts en séquence. Mais il « grandit vers la droite ». Nous avons donc le même problème qu’avec les rappels.
Les gens qui commencent à utiliser des promesses ne connaissent parfois pas le chaînage, alors ils l'écrivent de cette façon. Généralement, le chaînage est préféré.
Parfois, vous pouvez écrire .then
directement, car la fonction imbriquée a accès à la portée externe. Dans l'exemple ci-dessus, le rappel le plus imbriqué a accès à toutes les variables script1
, script2
, script3
. Mais c'est une exception plutôt qu'une règle.
Puisables
Pour être précis, un gestionnaire peut renvoyer non pas exactement une promesse, mais un objet dit « thenable » – un objet arbitraire qui a une méthode .then
. Elle sera traitée de la même manière qu’une promesse.
L’idée est que les bibliothèques tierces peuvent implémenter leurs propres objets « compatibles avec les promesses ». Ils peuvent avoir un ensemble étendu de méthodes, mais également être compatibles avec les promesses natives, car ils implémentent .then
.
Voici un exemple d'objet pouvant être visualisé :
classe Thenable { constructeur (num) { this.num = num; } alors (résoudre, rejeter) { alerte (résoudre); // fonction() { code natif } // résolution avec this.num*2 après 1 seconde setTimeout(() => solve(this.num * 2), 1000); // (**) } } nouvelle promesse (resolve => solve (1)) .then(résultat => { renvoyer un nouveau Thenable (résultat); // (*) }) .puis (alerte); // affiche 2 après 1000 ms
JavaScript vérifie l'objet renvoyé par le gestionnaire .then
à la ligne (*)
: s'il a une méthode appelable nommée then
, alors il appelle cette méthode fournissant des fonctions natives resolve
, reject
comme arguments (similaire à un exécuteur) et attend que l'une d'entre elles est appelé. Dans l'exemple ci-dessus, resolve(2)
est appelé après 1 seconde (**)
. Le résultat est ensuite transmis plus loin dans la chaîne.
Cette fonctionnalité nous permet d'intégrer des objets personnalisés avec des chaînes de promesses sans avoir à hériter de Promise
.
Dans la programmation front-end, les promesses sont souvent utilisées pour les requêtes réseau. Voyons donc un exemple étendu de cela.
Nous utiliserons la méthode fetch pour charger les informations sur l'utilisateur à partir du serveur distant. Il contient de nombreux paramètres facultatifs traités dans des chapitres séparés, mais la syntaxe de base est assez simple :
let promise = fetch(url);
Cela envoie une requête réseau à l' url
et renvoie une promesse. La promesse est résolue avec un objet response
lorsque le serveur distant répond avec des en-têtes, mais avant que la réponse complète ne soit téléchargée .
Pour lire la réponse complète, nous devons appeler la méthode response.text()
: elle renvoie une promesse qui se résout lorsque le texte intégral est téléchargé depuis le serveur distant, avec ce texte en conséquence.
Le code ci-dessous fait une requête à user.json
et charge son texte depuis le serveur :
récupérer('https://javascript.info/article/promise-chaining/user.json') // .then ci-dessous s'exécute lorsque le serveur distant répond .then(fonction(réponse) { //response.text() renvoie une nouvelle promesse qui se résout avec le texte complet de la réponse // quand il charge return réponse.text(); }) .then(fonction(texte) { // ...et voici le contenu du fichier distant alerte (texte); // {"name": "iliakan", "isAdmin": true} });
L'objet response
renvoyé par fetch
inclut également la méthode response.json()
qui lit les données distantes et les analyse au format JSON. Dans notre cas, c'est encore plus pratique, alors passons à cela.
Nous utiliserons également les fonctions fléchées par souci de concision :
// comme ci-dessus, mais réponse.json() analyse le contenu distant en JSON récupérer('https://javascript.info/article/promise-chaining/user.json') .then(réponse => réponse.json()) .then(user => alert(user.name)); // iliakan, j'ai obtenu le nom d'utilisateur
Faisons maintenant quelque chose avec l'utilisateur chargé.
Par exemple, nous pouvons faire une requête supplémentaire à GitHub, charger le profil utilisateur et afficher l'avatar :
// Faire une requête pour user.json récupérer('https://javascript.info/article/promise-chaining/user.json') // Chargez-le en json .then(réponse => réponse.json()) // Faire une requête à GitHub .then(user => fetch(`https://api.github.com/users/${user.name}`)) // Charge la réponse en json .then(réponse => réponse.json()) // Afficher l'image de l'avatar (githubUser.avatar_url) pendant 3 secondes (peut-être l'animer) .then(githubUser => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promesse-avatar-exemple"; document.body.append(img); setTimeout(() => img.remove(), 3000); // (*) });
Le code fonctionne ; voir les commentaires sur les détails. Cependant, il y a un problème potentiel, une erreur typique pour ceux qui commencent à utiliser les promesses.
Regardez la ligne (*)
: comment pouvons-nous faire quelque chose une fois que l'avatar a fini de s'afficher et a été supprimé ? Par exemple, nous aimerions afficher un formulaire permettant de modifier cet utilisateur ou autre chose. Pour l’instant, il n’y a aucun moyen.
Pour rendre la chaîne extensible, nous devons renvoyer une promesse qui se résout lorsque l'avatar finit de s'afficher.
Comme ça:
récupérer('https://javascript.info/article/promise-chaining/user.json') .then(réponse => réponse.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(réponse => réponse.json()) .then(githubUser => new Promise(function(resolve, rejet) { // (*) let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promesse-avatar-exemple"; document.body.append(img); setTimeout(() => { img.remove(); résoudre (githubUser); // (**) }, 3000); })) // se déclenche après 3 secondes .then(githubUser => alert(`Finished affichant ${githubUser.name}`));
Autrement dit, le gestionnaire .then
de la ligne (*)
renvoie désormais new Promise
, qui n'est réglée qu'après l'appel de resolve(githubUser)
dans setTimeout
(**)
. Le prochain .then
de la chaîne attendra cela.
En tant que bonne pratique, une action asynchrone doit toujours renvoyer une promesse. Cela permet de planifier des actions par la suite ; même si nous ne prévoyons pas d'étendre la chaîne maintenant, nous en aurons peut-être besoin plus tard.
Enfin, nous pouvons diviser le code en fonctions réutilisables :
fonction loadJson(url) { retourner chercher (url) .then(response => réponse.json()); } fonction loadGithubUser (nom) { return loadJson(`https://api.github.com/users/${name}`); } fonction showAvatar (githubUser) { retourner une nouvelle promesse (fonction (résoudre, rejeter) { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promesse-avatar-exemple"; document.body.append(img); setTimeout(() => { img.remove(); résoudre (githubUser); }, 3000); }); } // Utilisez-les : loadJson('https://javascript.info/article/promise-chaining/user.json') .then(utilisateur => loadGithubUser(user.name)) .then(showAvatar) .then(githubUser => alert(`Finished affichant ${githubUser.name}`)); //...
Si un gestionnaire .then
(ou catch/finally
, peu importe) renvoie une promesse, le reste de la chaîne attend qu'elle se règle. Lorsque c’est le cas, son résultat (ou son erreur) est transmis plus loin.
Voici une image complète :
Ces fragments de code sont-ils égaux ? En d’autres termes, se comportent-ils de la même manière dans toutes les circonstances, pour toutes les fonctions du gestionnaire ?
promesse.then(f1).catch(f2);
Contre:
promesse.then(f1, f2);
La réponse courte est : non, ils ne sont pas égaux :
La différence est que si une erreur se produit dans f1
, elle est gérée par .catch
ici :
promesse .puis(f1) .catch(f2);
…Mais pas ici :
promesse .puis(f1, f2);
C'est parce qu'une erreur est transmise dans la chaîne et que dans le deuxième morceau de code, il n'y a pas de chaîne en dessous de f1
.
En d’autres termes, .then
transmet les résultats/erreurs au prochain .then/catch
. Ainsi, dans le premier exemple, il y a un catch
ci-dessous, et dans le second, il n'y en a pas, donc l'erreur n'est pas gérée.