Imaginez que vous êtes un chanteur de renom et que vos fans vous demandent jour et nuit votre prochaine chanson.
Pour obtenir un peu de soulagement, vous promettez de le leur envoyer dès sa publication. Vous donnez une liste à vos fans. Ils peuvent renseigner leur adresse e-mail, de sorte que lorsque la chanson sera disponible, tous les abonnés la recevront instantanément. Et même si quelque chose tourne mal, par exemple un incendie dans le studio, qui fait que vous ne pouvez pas publier la chanson, ils en seront quand même informés.
Tout le monde est content : vous, parce que les gens ne vous pressent plus, et les fans, parce que la chanson ne leur manquera pas.
Il s'agit d'une analogie réelle avec des choses que nous avons souvent en programmation :
Un « code producteur » qui fait quelque chose et prend du temps. Par exemple, du code qui charge les données sur un réseau. C'est un « chanteur ».
Un « code consommateur » qui veut le résultat du « code producteur » une fois qu'il est prêt. De nombreuses fonctions peuvent nécessiter ce résultat. Ce sont les « fans ».
Une promesse est un objet JavaScript spécial qui relie le « code producteur » et le « code consommateur ». Pour reprendre notre analogie : il s'agit de la « liste d'abonnement ». Le « code producteur » prend le temps dont il a besoin pour produire le résultat promis, et la « promesse » rend ce résultat disponible à tout le code souscrit lorsqu'il est prêt.
L'analogie n'est pas très précise, car les promesses JavaScript sont plus complexes qu'une simple liste d'abonnement : elles comportent des fonctionnalités et des limitations supplémentaires. Mais c'est bien pour commencer.
La syntaxe du constructeur pour un objet de promesse est :
let promise = new Promise (fonction (résolution, rejet) { // exécuteur (le code producteur, "chanteur") });
La fonction transmise à new Promise
est appelée exécuteur . Lorsqu'une new Promise
est créée, l'exécuteur s'exécute automatiquement. Il contient le code producteur qui devrait finalement produire le résultat. En termes d’analogie ci-dessus : l’exécuteur testamentaire est le « chanteur ».
Ses arguments resolve
et reject
sont des rappels fournis par JavaScript lui-même. Notre code se trouve uniquement à l'intérieur de l'exécuteur.
Lorsque l'exécuteur obtient le résultat, que ce soit tôt ou tard, peu importe, il doit appeler l'un de ces rappels :
resolve(value)
— si le travail est terminé avec succès, avec value
de résultat .
reject(error)
— si une erreur s'est produite, error
est l'objet d'erreur.
Donc pour résumer : l'exécuteur s'exécute automatiquement et tente d'effectuer un travail. Une fois la tentative terminée, il appelle resolve
si elle a réussi ou reject
s'il y a eu une erreur.
L'objet promise
renvoyé par le new Promise
possède ces propriétés internes :
state
- initialement "pending"
, puis passe soit à "fulfilled"
lorsque resolve
est appelée, soit "rejected"
lorsque reject
est appelé.
result
- initialement undefined
, puis change en value
lorsque resolve(value)
est appelé ou error
lorsque reject(error)
est appelé.
Ainsi, l’exécuteur finit par déplacer promise
vers l’un de ces états :
Nous verrons plus tard comment les « fans » peuvent souscrire à ces changements.
Voici un exemple de constructeur de promesse et de fonction d'exécution simple avec « production de code » qui prend du temps (via setTimeout
) :
let promise = new Promise (fonction (résolution, rejet) { // la fonction est exécutée automatiquement lorsque la promesse est construite // après 1 seconde, signale que le travail est terminé avec le résultat "terminé" setTimeout(() => solve("done"), 1000); });
Nous pouvons voir deux choses en exécutant le code ci-dessus :
L'exécuteur est appelé automatiquement et immédiatement (par new Promise
).
L'exécuteur reçoit deux arguments : resolve
et reject
. Ces fonctions sont prédéfinies par le moteur JavaScript, nous n'avons donc pas besoin de les créer. Nous ne devrions en appeler qu'un seul lorsque nous sommes prêts.
Après une seconde de « traitement », l'exécuteur appelle resolve("done")
pour produire le résultat. Cela change l'état de l'objet promise
:
C’était un exemple de réussite d’un travail, de « promesse tenue ».
Et maintenant un exemple de l'exécuteur testamentaire rejetant la promesse avec une erreur :
let promise = new Promise (fonction (résoudre, rejeter) { // après 1 seconde signalant que le travail est terminé avec une erreur setTimeout(() => rejet(new Error("Whoops!")), 1000); });
L'appel à reject(...)
déplace l'objet de promesse vers l'état "rejected"
:
Pour résumer, l'exécuteur doit effectuer un travail (généralement quelque chose qui prend du temps), puis appeler resolve
ou reject
pour modifier l'état de l'objet de promesse correspondant.
Une promesse qui est soit résolue, soit rejetée est dite « réglée », par opposition à une promesse initialement « en attente ».
Il ne peut y avoir qu'un seul résultat ou une erreur
L'exécuteur ne doit appeler qu'une seule resolve
ou un seul reject
. Tout changement d'état est définitif.
Tous les autres appels de resolve
et reject
sont ignorés :
let promise = new Promise (fonction (résolution, rejet) { résoudre("fait"); rejeter(nouvelle erreur("…")); // ignoré setTimeout(() => résoudre("…")); // ignoré });
L’idée est qu’un travail effectué par l’exécuteur peut n’avoir qu’un seul résultat ou une erreur.
De plus, resolve
/ reject
n’attend qu’un seul argument (ou aucun) et ignorera les arguments supplémentaires.
Rejeter avec des objets Error
En cas de problème, l'exécuteur doit appeler reject
. Cela peut être fait avec n'importe quel type d'argument (tout comme resolve
). Mais il est recommandé d'utiliser des objets Error
(ou des objets qui héritent de Error
). La raison en sera bientôt évidente.
Appel immédiat resolve
/ reject
En pratique, un exécuteur fait généralement quelque chose de manière asynchrone et appelle resolve
/ reject
après un certain temps, mais ce n'est pas obligatoire. Nous pouvons également appeler resolve
ou reject
immédiatement, comme ceci :
let promise = new Promise (fonction (résolution, rejet) { // ne prend pas notre temps pour faire le travail résoudre (123); // donne immédiatement le résultat : 123 });
Par exemple, cela peut se produire lorsque nous commençons à effectuer un travail, mais que nous constatons ensuite que tout est déjà terminé et mis en cache.
C'est très bien. Nous avons immédiatement une promesse résolue.
L' state
et result
sont internes
L' state
des propriétés et result
de l'objet Promise sont internes. Nous ne pouvons pas y accéder directement. Nous pouvons utiliser les méthodes .then
/ .catch
/ .finally
pour cela. Ils sont décrits ci-dessous.
Un objet Promise sert de lien entre l'exécuteur (le « code producteur » ou « chanteur ») et les fonctions consommatrices (les « fans »), qui recevront le résultat ou l'erreur. Les fonctions consommatrices peuvent être enregistrées (abonnées) à l'aide des méthodes .then
et .catch
.
Le plus important et fondamental est .then
.
La syntaxe est :
promesse.puis( function(result) { /* gérer un résultat réussi */ }, function(error) { /* gérer une erreur */ } );
Le premier argument de .then
est une fonction qui s'exécute lorsque la promesse est résolue et reçoit le résultat.
Le deuxième argument de .then
est une fonction qui s'exécute lorsque la promesse est rejetée et reçoit l'erreur.
Par exemple, voici une réaction à une promesse résolue avec succès :
let promise = new Promise (fonction (résolution, rejet) { setTimeout(() => solve("done!"), 1000); }); // solve exécute la première fonction dans .then promesse.puis( result => alert(result), // affiche "terminé!" après 1 seconde erreur => alerte(erreur) // ne s'exécute pas );
La première fonction a été exécutée.
Et en cas de rejet, le deuxième :
let promise = new Promise (fonction (résolution, rejet) { setTimeout(() => rejet(new Error("Whoops!")), 1000); }); // rejet exécute la deuxième fonction dans .then promesse.puis( result => alert(result), // ne s'exécute pas error => alert(error) // affiche "Erreur : Oups !" après 1 seconde );
Si nous ne nous intéressons qu'aux réussites, alors nous ne pouvons fournir qu'un seul argument de fonction à .then
:
let promise = new Promise(resolve => { setTimeout(() => solve("done!"), 1000); }); promesse.then(alerte); // affiche "terminé!" après 1 seconde
Si nous ne nous intéressons qu'aux erreurs, nous pouvons utiliser null
comme premier argument : .then(null, errorHandlingFunction)
. Ou nous pouvons utiliser .catch(errorHandlingFunction)
, ce qui est exactement la même chose :
let promise = new Promise((résoudre, rejeter) => { setTimeout(() => rejet(new Error("Whoops!")), 1000); }); // .catch(f) est identique à promise.then(null, f) promesse.catch(alerte); // affiche "Erreur : Oups !" après 1 seconde
L'appel .catch(f)
est un analogue complet de .then(null, f)
, c'est juste un raccourci.
Tout comme il y a une clause finally
dans un try {...} catch {...}
normal, il y a finally
dans les promesses.
L'appel .finally(f)
est similaire à .then(f, f)
dans le sens où f
s'exécute toujours, lorsque la promesse est réglée : qu'elle soit résolue ou rejetée.
L'idée de finally
est de configurer un gestionnaire pour effectuer le nettoyage/finalisation une fois les opérations précédentes terminées.
Par exemple, arrêter les indicateurs de chargement, fermer les connexions qui ne sont plus nécessaires, etc.
Considérez-le comme une fin de fête. Peu importe qu'une fête soit bonne ou mauvaise, combien d'amis y participaient, nous devons toujours (ou du moins devrions) faire un nettoyage après.
Le code peut ressembler à ceci :
nouvelle promesse ((résoudre, rejeter) => { /* faire quelque chose qui prend du temps, puis appeler solve ou peut-être rejeter */ }) // s'exécute lorsque la promesse est réglée, peu importe avec succès ou non .finally(() => indicateur d'arrêt du chargement) // donc l'indicateur de chargement est toujours arrêté avant de continuer .then (résultat => afficher le résultat, err => afficher l'erreur)
Veuillez noter que finally(f)
n'est pas exactement un alias de then(f,f)
.
Il existe des différences importantes :
Un gestionnaire finally
n'a aucun argument. finally
nous ne savons pas si la promesse est tenue ou non. Ce n'est pas grave, car notre tâche consiste généralement à effectuer des procédures de finalisation « générales ».
Veuillez jeter un œil à l'exemple ci-dessus : comme vous pouvez le voir, le gestionnaire finally
n'a aucun argument et le résultat de la promesse est géré par le gestionnaire suivant.
Un gestionnaire finally
« transmet » le résultat ou l’erreur au prochain gestionnaire approprié.
Par exemple, ici le résultat est transmis finally
à then
:
nouvelle promesse ((résoudre, rejeter) => { setTimeout(() => solve("valeur"), 2000); }) .finally(() => alert("Promise ready")) // se déclenche en premier .then(résultat => alerte(résultat)); // <-- .puis affiche "valeur"
Comme vous pouvez le voir, la value
renvoyée par la première promesse est transmise finally
à la suivante then
.
C'est très pratique, car finally
n'est pas destiné à traiter un résultat promis. Comme indiqué, c'est un endroit pour effectuer un nettoyage générique, quel que soit le résultat.
Et voici un exemple d'erreur, pour que nous puissions voir comment elle est finally
transmise pour catch
:
nouvelle promesse ((résoudre, rejeter) => { lancer une nouvelle erreur("erreur"); }) .finally(() => alert("Promise ready")) // se déclenche en premier .catch(erreur => alerte(erreur)); // <-- .catch affiche l'erreur
Un gestionnaire finally
ne devrait pas non plus rien renvoyer. Si tel est le cas, la valeur renvoyée est ignorée en silence.
La seule exception à cette règle est lorsqu'un gestionnaire finally
génère une erreur. Cette erreur est ensuite transmise au gestionnaire suivant, au lieu de tout résultat précédent.
Pour résumer :
Un gestionnaire finally
n'obtient pas le résultat du gestionnaire précédent (il n'a aucun argument). Ce résultat est transmis au prochain gestionnaire approprié.
Si un gestionnaire finally
renvoie quelque chose, celui-ci est ignoré.
Lorsqu'une erreur est finally
générée, l'exécution est dirigée vers le gestionnaire d'erreurs le plus proche.
Ces fonctionnalités sont utiles et permettent aux choses de fonctionner correctement si nous les utilisons finally
comme elles sont censées être utilisées : pour les procédures de nettoyage génériques.
Nous pouvons attacher des gestionnaires aux promesses réglées
Si une promesse est en attente, les gestionnaires .then/catch/finally
attendent son résultat.
Parfois, il se peut qu'une promesse soit déjà réglée lorsque nous y ajoutons un gestionnaire.
Dans ce cas, ces gestionnaires s’exécutent immédiatement :
// la promesse est résolue immédiatement après la création let promise = new Promise(resolve => solve("done!")); promesse.then(alerte); // fait! (apparaît en ce moment)
Notez que cela rend les promesses plus puissantes que le scénario réel de la « liste d’abonnement ». Si le chanteur a déjà sorti sa chanson et qu'une personne s'inscrit sur la liste d'abonnement, elle ne recevra probablement pas cette chanson. Les inscriptions en vrai doivent être effectuées avant l'événement.
Les promesses sont plus flexibles. Nous pouvons ajouter des gestionnaires à tout moment : si le résultat est déjà là, ils s'exécutent simplement.
Voyons ensuite des exemples plus pratiques de la façon dont les promesses peuvent nous aider à écrire du code asynchrone.
Nous avons la fonction loadScript
pour charger un script du chapitre précédent.
Voici la variante basée sur le rappel, juste pour nous le rappeler :
fonction loadScript (src, rappel) { let script = document.createElement('script'); script.src = src; script.onload = () => rappel (null, script); script.onerror = () => callback(new Error(`Erreur de chargement de script pour ${src}`)); document.head.append(script); }
Réécrivons-le en utilisant Promises.
La nouvelle fonction loadScript
ne nécessitera pas de rappel. Au lieu de cela, il créera et renverra un objet Promise qui se résoudra une fois le chargement terminé. Le code externe peut y ajouter des gestionnaires (fonctions d'abonnement) en utilisant .then
:
fonction loadScript(src) { retourner une nouvelle promesse (fonction (résoudre, rejeter) { let script = document.createElement('script'); script.src = src; script.onload = () => résoudre (script); script.onerror = () => rejet(new Error(`Erreur de chargement de script pour ${src}`)); document.head.append(script); }); }
Usage:
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); promesse.puis( script => alert(`${script.src} est chargé !`), erreur => alerte(`Erreur : ${error.message}`) ); promise.then(script => alert('Un autre gestionnaire...'));
Nous pouvons immédiatement constater quelques avantages par rapport au modèle basé sur le rappel :
Promesses | Rappels |
---|---|
Les promesses nous permettent de faire les choses dans l’ordre naturel. Tout d’abord, nous exécutons loadScript(script) , .then nous écrivons quoi faire avec le résultat. | Nous devons avoir une fonction callback à notre disposition lors de l'appel loadScript(script, callback) . En d’autres termes, nous devons savoir quoi faire du résultat avant que loadScript ne soit appelé. |
Nous pouvons .then faire appel à une Promesse autant de fois que nous le souhaitons. A chaque fois, nous ajoutons un nouveau « fan », une nouvelle fonction d'abonnement, à la « liste d'abonnement ». Plus d’informations à ce sujet dans le chapitre suivant : Enchaînement des promesses. | Il ne peut y avoir qu'un seul rappel. |
Les promesses nous offrent donc un meilleur flux de code et une meilleure flexibilité. Mais il y a plus. Nous verrons cela dans les prochains chapitres.
Quel est le résultat du code ci-dessous ?
let promise = new Promise (fonction (résolution, rejet) { résoudre (1); setTimeout(() => résoudre(2), 1000); }); promesse.then(alerte);
Le résultat est : 1
.
Le deuxième appel à resolve
est ignoré, car seul le premier appel au reject/resolve
est pris en compte. Les autres appels sont ignorés.
La fonction intégrée setTimeout
utilise des rappels. Créez une alternative basée sur des promesses.
La fonction delay(ms)
devrait renvoyer une promesse. Cette promesse devrait être résolue après ms
millisecondes, afin que nous puissions y ajouter .then
, comme ceci :
délai de fonction (ms) { // ton code } delay(3000).then(() => alert('s'exécute après 3 secondes'));
délai de fonction (ms) { return new Promise(resolve => setTimeout(resolve, ms)); } delay(3000).then(() => alert('s'exécute après 3 secondes'));
Veuillez noter que dans cette tâche, resolve
est appelée sans arguments. Nous ne renvoyons aucune valeur de delay
, assurons simplement le delay.
Réécrivez la fonction showCircle
dans la solution de la tâche Cercle animé avec rappel afin qu'elle renvoie une promesse au lieu d'accepter un rappel.
Le nouvel usage :
showCircle(150, 150, 100).then(div => { div.classList.add('message-ball'); div.append("Bonjour tout le monde !"); });
Prenez la solution de la tâche Cercle animé avec rappel comme base.
Ouvrez la solution dans un bac à sable.