
Les promesses en tant que concept sont difficiles à appréhender parce qu'il faut comprendre le mécanisme de la boucle d'événements JavaScript, les callbacks et la programmation asynchrone (async await) pour pouvoir comprendre les promesses. Dans cet article, je vais expliquer toutes ces choses.
Il y a quelques jours, un de mes bons amis a envoyé ce meme à notre groupe WhatsApp :
Je me suis dit qu'il fallait écrire un article sur les promesses dans JavaScript.
Elles peuvent être un peu difficiles à avaler au début, mais plus on travaille avec elles, plus on les comprend.
Les promesses en tant que concept sont difficiles à appréhender car il faut comprendre :
- le mécanisme de la boucle d'événements,
- les callbacks,
- la programmation asynchrone (async await)
Le bon vieux temps
Je me souviens du bon vieux temps où l'on codait tout en HTML/CSS. Plus tard, c'était un peu de jQuery, ici et là quelques appels d'API et c'est tout.
Aujourd'hui, les données sont récupérées via les points d'extrémité de l'API, ce qui signifie que vous devez gérer un grand nombre de requêtes HTTP dans votre code. Cela implique que vous devez savoir comment travailler avec la programmation asynchrone.
Nous savons tous que JavaScript est un langage de programmation monotâche et qu'il utilise un modèle d'exécution synchrone. En clair, il peut traiter les instructions du code une par une.
Imaginez que vous deviez attendre que les données soient extraites de l'API pour écrire du texte dans le champ de saisie ou cliquer sur le bouton du site web. C'est ce qui se passerait si les requêtes HTTP étaient exécutées de manière synchrone. Ce comportement est également connu sous le nom de blocage, car il empêche les utilisateurs d'interagir avec le site web.
Pour remédier à ce comportement de blocage, les navigateurs web ont intégré des API spéciales. JavaScript peut les utiliser pour exécuter du code de manière asynchrone. Ainsi, les opérations lourdes, telles que l'extraction de données de l'API, peuvent être exécutées en parallèle avec d'autres opérations synchrones. Ainsi, les utilisateurs ne sont pas bloqués et peuvent utiliser le site web sans problème.
En tant que développeur JavaScript, vous devez savoir comment gérer ces opérations. Vous devez également comprendre le comportement asynchrone, le mécanisme de la boucle d'événements, les rappels et les promesses.
Boucle d'événements JavaScript
Comme indiqué précédemment, JavaScript exécute les instructions de code une par une. Nous pouvons le confirmer avec un exemple de code qui imprime nos Pokemons préférés dans l'ordre alphabétique :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function chooseBulbasaur() { console.log('I choose you Bulbasaur!') } function chooseCharmander() { console.log('I choose you Charmander!') } function chooseSquirtle() { console.log('I choose you Squirtle!') } chooseBulbasaur() chooseCharmander() chooseSquirtle() // prints: // I choose you Bulbasaur! // I choose you Charmander! // I choose you Squirtle! |
Si nous modifions ce code pour utiliser l'API web asynchrone afin d'exécuter du code, comme setTimeout, les choses vont changer :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | function chooseBulbasaur() { console.log('I choose you Bulbasaur!') } function chooseCharmander() { setTimeout(() => { console.log('I choose you Charmander!') }, 0) } function chooseSquirtle() { console.log('I choose you Squirtle!') } chooseBulbasaur() chooseCharmander() chooseSquirtle() // prints: // I choose you Bulbasaur! // I choose you Squirtle! // I choose you Charmander! |
Nous savons que setTimeout est une fonction qui reçoit 2 arguments :
- une fonction qui doit être exécutée après X millisecondes. Dans l'exemple ci-dessus, il s'agit d'une fonction qui enregistre ""I choose you Charmander"" sur la console
- un nombre de millisecondes
Vous vous demandez peut-être si, dans l'exemple ci-dessus, le nombre de millisecondes est fixé à 0, ce qui signifie que la fonction devrait être exécutée immédiatement. Pourtant, ce n'est pas le cas.
setTimeout est une fonction qui est exécutée de manière asynchrone parce qu'elle exécute du code après X millisecondes.
Il serait très mauvais qu'elle soit exécutée de manière synchrone. En effet, les utilisateurs devraient alors attendre X millisecondes avant de pouvoir utiliser à nouveau votre site web. Donc, si vous fixez 0 ou 10 millisecondes, cela n'a pas d'importance pour JavaScript. Si le code est exécuté de manière asynchrone, cela signifie qu'il est exécuté juste après les fonctions synchrones de premier niveau.
JavaScript sait quelle instruction doit être exécutée ensuite grâce au mécanisme de la boucle d'événements. Pour pouvoir comprendre la boucle d'événements, vous devez comprendre des concepts tels que la pile et la file d'attente. Ceux-ci sont utilisés derrière les rideaux.
Pile et file d'attente JavaScript (FIFO et LIFO)
Vous pouvez imaginer une pile comme un tableau normal qui supprime des éléments selon le principe du "dernier entré, premier sorti" (LIFO). Cela signifie que vous ne pouvez retirer des éléments qu'à partir de la fin de la pile.
La pile est importante en JavaScript car elle contient le contexte de la fonction en cours d'exécution. JavaScript supprime les éléments de la pile un par un et passe au suivant jusqu'à ce qu'il ne reste plus rien.
Écrivons comment la pile JavaScript exécute les fonctions de notre exemple synchrone :
- Insérer la fonction chooseBulbasaur dans la pile, l'exécuter, imprimer « I choose you Bulbasaur ! » sur la console et enfin retirer chooseBulbasaur de la pile.
- Insérer la fonction chooseCharmander dans la pile, l'exécuter, imprimer « I choose you Charmander ! » sur la console et enfin retirer chooseCharmander de la pile.
- Insérer la fonction chooseSquirtle dans la pile, l'exécuter, imprimer « I choose you Squirtle ! » sur la console et enfin retirer chooseSquirtle de la pile.
C'est très simple et il n'y a rien de compliqué. Voyons maintenant comment se comporte le code asynchrone :
- Insérer la fonction chooseBulbasaur dans la pile, l'exécuter, imprimer « I choose you Bulbasaur ! » sur la console et enfin supprimer chooseBulbasaur de la pile.
- Ajouter la fonction chooseCharmander à la pile. Exécutez-la.
- Insérer la fonction setTimeout dans la pile.
- Exécutez setTimeout.
- setTimeout démarre un timer et ajoute la fonction anonyme qu'il contenait à la file d'attente.
- Retirer setTimeout de la pile
- Ajouter la fonction chooseSquirtle à la pile, l'exécuter, imprimer « I choose you Squirtle ! » sur la console et enfin retirer chooseSquirtle de la pile.
- Une fois que tout est terminé, la boucle événementielle JavaScript vérifie la présence d'un contexte supplémentaire dans la file d'attente.
- Elle trouvera la fonction anonyme qui a été précédemment ajoutée par setTimeout.
- Elle ajoute cette fonction à la pile.
- La pile récupère cette fonction.
- L'éxécute.
- Imprime « I choose you Charmander ! » sur la console,
- Enfin, supprimer la fonction anonyme de la pile
Si vous êtes plutôt du genre vidéo, cette video m'a beaucoup aidé à comprendre le mécanisme de la boucle événementielle JavaScript.
Si vous avez des difficultés à comprendre FIFO et LIFO, cette image l'explique assez bien :
Nous avons déjà mentionné la file d'attente, alors expliquons-la.
JavaScript utilise également une file d'attente (file d'attente de tâches) en arrière-plan. Vous pouvez imaginer la file d'attente comme le salon ou la salle d'attente des fonctions, tandis que la pile est le bureau de JavaScript. Lorsque la pile est vide, JavaScript vérifie s'il y a quelque chose dans la file d'attente. Si la pile est vide et que quelque chose se trouve dans la file d'attente, JavaScript l'appelle et l'exécute.
C'est exactement ce qui s'est passé dans l'exemple asynchrone ci-dessus. La fonction anonyme dans setTimeout a été exécutée après toutes les autres fonctions de niveau supérieur.
Il est important de noter que 0 milliseconde ne signifie pas que la fonction anonyme sera exécutée après 0 milliseconde. Cela signifie qu'après X millisecondes, elle sera ajoutée à la file d'attente.
Si la fonction était exécutée dans la pile juste après X millisecondes, elle pourrait interrompre la fonction actuellement exécutée dans la pile. Nous aurions alors toutes sortes de problèmes. Avec la file d'attente, nous n'avons pas ces problèmes.
Rappels JavaScript
Les callbacks ont d'abord été essayés pour résoudre les problèmes liés au code asynchrone. La solution est très simple.
Vous passez la fonction de rappel comme argument à une fonction d'ordre supérieur. Cette fonction exécute alors la fonction de rappel en argument au moment opportun.
Codons la solution de rappel pour l'exemple précédent :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | function chooseBulbasaur() { console.log('I choose you Bulbasaur!') } function chooseCharmander(callback) { setTimeout(() => { console.log('I choose you Charmander!') // execute callback function callback() }, 0) } function chooseSquirtle() { console.log('I choose you Squirtle!') } chooseBulbasaur() chooseCharmander(chooseSquirtle) // prints: // I choose you Bulbasaur! // I choose you Charmander! // I choose you Squirtle! |
Il est important de se rappeler que les callbacks ne sont pas asynchrones, mais que setTimeout l'est. Les callbacks ne sont qu'un mécanisme permettant d'être notifié lorsque du code asynchrone est exécuté. Vous pouvez alors décider de ce que vous devez faire après un succès ou une erreur.
Les rappels sont très bien, mais ils présentent un autre aspect. Ils peuvent nuire à la lisibilité du code à tel point qu'ils ont un nom très particulier pour cela.
Vous avez probablement entendu parler de « l'enfer des callbacks » ou de la « pyramide du destin », mais voyons ce qu'il en est :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function pyramidOfDoom() { setTimeout(() => { console.log('I choose you Bulbasaur!') setTimeout(() => { console.log('I choose you Charmander!') setTimeout(() => { console.log('I choose you Squirtle!') }, 300) }, 1200) }, 600) } // prints: // I choose you Bulbasaur! // I choose you Charmander! // I choose you Squirtle! |
Vous voyez ces emboîtements. Ce n'est pas joli, n'est-ce pas ?
Eh bien, ce n'est certainement pas le pire des cas, mais cela peut être bien pire. Imaginez que tous ces setTimeouts soient des fonctions séparées, avec des arguments de réponse et d'erreur. Il faut alors gérer les erreurs et faire des choses avec les réponses.
En raison de tous ces problèmes, la norme JavaScript ES6 a introduit les promesses pour résoudre ces problèmes.
Promesses JavaScript
Une promesse est simplement un objet qui peut renvoyer une valeur dans le futur. Elle définit la réalisation d'une opération asynchrone. Elle atteint le même objectif que la fonction callback mais offre plus d'outils et a une syntaxe plus propre.
La promesse peut avoir 3 états :
- fulfilled - Succès, la promesse a été résolue
- rejected - Échec, la promesse a été rejetée
- pending - état par défaut avant d'être résolue ou rejetée
Voyons des exemples de code pour tous ces états :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | const resolvedPromise = new Promise((resolve, reject) => { resolve('I am resolved!') }) // Promise { <state>: "fulfilled", <value>: "I am resolved!" } const rejectedPromise = new Promise((resolve, reject) => { reject('I am rejected!') }) // Promise { <state>: "rejected", <reason>: "I am rejected!" } const promise = new Promise((resolve, reject) => {}) // Promise { <state>: "pending", value: undefined } |
Comme vous pouvez le voir, vous pouvez initialiser une promesse avec un nouveau mot-clé et vous devez passer une fonction à l'intérieur. Cette fonction doit avoir des paramètres resolve et reject :
- resolve - gère le scénario de réussite de la promesse.
- reject - gère le scénario d'échec de la promesse.
Maintenant que vous savez comment créer une promesse, voyons quelques exemples de la façon dont vous pouvez consommer une promesse.
Il s'agit d'un scénario plus fréquent dans votre travail quotidien que la création d'une promesse.
Les fonctions que vous pouvez utiliser pour consommer une promesse :
- .then() - gère le scénario de résolution, exécute la fonction onFullfiled de manière asynchrone
- .catch() - gère le scénario de rejet, exécute la fonction onRejected de manière asynchrone
- .finally() - gère le scénario lorsque la promesse est réglée, la fonction onFinally est exécutée de manière asynchrone.
Jetons un coup d'œil à cet exemple :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | const choosePikachu = new Promise((resolve, reject) => { setTimeout(() => resolve('Pikachu'), 2000) }) choosePikachu.then((response) => { console.log(response) }) // prints "Pikachu" |
Comme vous pouvez le constater, vous pouvez facilement consommer des promesses avec la syntaxe .then. Ceci imprimera le message « Pikachu » dans la console après 2000 ms.
N'oubliez pas que vous pouvez également enchaîner les promesses :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | choosePikachu .then((firstForm) => { // Return a new value for the next .then return firstForm + ' evolves to Raichu!' }) .then((secondForm) => { console.log(secondForm) }) // prints "Pikachu evolves to Raichu" |
Cela s'avère utile dans de nombreuses situations, par exemple lorsque vous récupérez des données d'une API externe :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | fetch('https://pokeapi.co/api/v2/pokemon/pikachu') .then((resp) => { return resp.json() }) .then((data) => { console.log(data) }) .catch((err) => { console.error(err) }) |
Mais aussi, toutes les promesses ne seront pas exécutées. Certaines d'entre elles résulteront en des erreurs, vous devez donc les gérer également avec la syntaxe .catch.
Voici un exemple :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | function getPokemons(isSuccess) { return new Promise((resolve, reject) => { setTimeout(() => { if (isSuccess) { resolve([ { id: 1, name: 'Bulbasaur' }, { id: 2, name: 'Charmander' }, { id: 3, name: 'Squirtle' }, ]) } else { reject('An error occurred on fetching pokemons!') } }, 3000) }) } getPokemons(false) .then((response) => { console.log(response) }) .catch((error) => { console.error(error) }) // prints "An error occurred on fetching pokemons!" |
Comme vous pouvez le voir, la fonction getPokemons renvoie une promesse. Cette promesse ne renverra les données relatives aux pokémons que si le paramètre isSuccess est vrai. Dans le cas contraire, une erreur se produira.
Après la définition de la fonction, nous exécutons la fonction getPokemons. La promesse est consommée avec les fonctions encaînées .then et .catch. La promesse est rejetée car l'argument de la fonction getPokemons est [/C]false[/C]. La promesse sera rejetée. Il en résultera le message « An error occurred on fetching pokemons ».
Il s'agit du scénario le plus fréquent de la manière dont vous pouvez gérer les demandes d'API dans votre application. Par exemple, pour récupérer les données du back-end et les afficher dans votre application.
Async await
Dans la norme ES 2017, JavaScript a introduit les mots-clés async et await. Ces mots-clés sont très utiles si vous ne voulez pas utiliser la syntaxe .then. Avec async await, vous avez l'impression que le code est synchrone, alors qu'il ne l'est pas.
La syntaxe est un peu différente, mais en arrière-plan, le code utilise toujours les bonnes vieilles promesses :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 | async function fetchPokemon() { return { name: 'Pikachu' } } console.log(fetchPokemon()) // prints Promise { <state>: "fulfilled", <value>: {name: 'Pikachu'} } |
Comme vous pouvez le voir, la valeur de retour est toujours Promise. Cela signifie que vous pouvez utiliser la syntaxe .then sur les fonctions asynchrones, comme ceci :
Code : | Sélectionner tout |
1 2 3 | fetchPokemon().then((resp) => console.log(resp)) // prints { name: 'Pikachu' } |
Pour le mot-clé await, vous pouvez l'utiliser dans une fonction asynchrone lorsque vous voulez attendre que la Promesse soit remplie avant de passer à la ligne suivante.
Voici un exemple :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | async function fetchPokemon() { const resp = await fetch('https://pokeapi.co/api/v2/pokemon/pikachu') const pokemon = await resp.json() console.log(pokemon) } fetchPokemon() // prints // { // abilities: (2) […], // base_experience: 112, // forms: (1) […], // game_indices: (20) // name: "pikachu" // ... // } |
Il est également utile de noter que nous pouvons gérer les erreurs avec un bloc try-catch classique au lieu de .catch() que nous avons utilisé avec la syntaxe .then() :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | async function fetchPokemon() { try { const resp = await fetch('https://pokeapi.co/api/v2/pokemon/pikachu') const pokemon = await resp.json() console.log(pokemon) } catch (err) { console.error(err) } } |
Conclusion
La plupart des projets modernes utilisent la syntaxe async-await. Mais pour pouvoir la comprendre pleinement, les développeurs doivent tout savoir sur le code asynchrone.
Par exemple, vous utiliserez parfois Promise.all et vous ne pourrez pas le faire tant que vous n'aurez pas compris tout ce qui a été mentionné précédemment.
Dans cet article, nous avons découvert comment l'environnement hôte utilise la boucle d'événements pour gérer les ordres d'exécution du code à l'aide de la pile et de la file d'attente. Nous avons exploré trois méthodes pour gérer les événements asynchrones :
- les callbacks,
- les promesses,
- la syntaxe async/await.
Nous avons également appris à utiliser l'API Web fetch pour les actions asynchrones.
Source : Everything Everywhere All at Once: JavaScript Promises
Et vous ?


Voir aussi :



Vous avez lu gratuitement 0 articles depuis plus d'un an.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.