Les promesses¶
La programmation asynchrone a pris une place prépondérante dans le développement d’application en JavaScript. La raison principale vient du fait que l’interpréteur JavaScript intégré à tous les navigateurs Web ne supporte pas le parallélisme : le code JavaScript dans une page Web s’exécute dans un thread unique. Cela implique qu’il n’est pas possible de réaliser du code bloquant ou des boucles infinies. Pour le développement d’application serveur en JavaScript avec Node.js, le problème reste le même puisque Node.js reprend le même principe d’exécution dans un thread unique. Habituellement en programmation, les appels au système de fichiers ou les appels réseaux sont des appels bloquants. En JavaScript, comme un appel bloquant pénalise l’ensemble de la page Web ou de l’application, ce type d’appels est remplacé par un appel asynchrone.
La première façon de réaliser des appels asynchrones en JavaScript est d’utiliser des méthodes de callback passées en paramètres de l’appel asynchrone. Par exemple, le code suivant permet depuis une page Web de récupérer des données JSON sur un serveur distant (appel réseau) :
let xhr = new XMLHttpRequest();
xhr.open("GET", "http://monsite.fr/data.json");
xhr.responseType = "json";
xhr.addEventListener("load", function() {
let data = xhr.response;
// ...
});
xhr.send();
Dans l’exemple, ci-dessus on définit une fonction de callback pour l’événement
load
qui sera appelée lorsque les données auront été récupérées sur le réseau.
Un autre exemple pour une implémentation avec le serveur Node.js :
let http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Bienvenue sur mon serveur');
}).listen(8080);
On passe une fonction de callback à la fonction createServer
qui sera
appelée pour chaque requête reçue.
L’inconvénient des fonctions de callback est qu’elles rendent le code difficile à lire et donc difficile à maintenir. ES6 a introduit le modèle des promesses (promises). L’objectif est de produire du code asynchrone plus simple et plus lisible sous la forme d’une chaîne d’appels à des fonctions.
La classe Promise¶
ES6 fournit la classe Promise
qui attend en paramètre une fonction. Cette
dernière prend deux paramètres : resolve et reject. resolve est la
fonction à appeler quand la tache asynchrone est finie et reject quand la
tache asynchrone a échoué ou ne peut pas être exécutée.
Nous pouvons facilement transformer un appel ajax avec XMLHttpRequest
sous
la forme d’une promesse :
const URL = "http://quelquepart.com/data.json";
let ajax = new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open("GET", URL);
request.addEventListener("load", function(){
if(request.status === 200) {
resolve(request.responseText);
} else {
reject("Erreur du serveur : " + request.status);
}
}, false);
request.addEventListener("error", function(){
reject("La requête ajax a échoué");
}, false);
request.send();
});
La méthode resolve est appelée en passant en paramètre le résultat de la tache. La méthode reject est appelée en passant un message d’erreur.
Une promesse dispose de la méthode then
qui accepte en paramètre une méthode
de callback fournissant le traitement à réaliser à la fin de la tache, c’est-à-dire
lorsque la méthode resolve sera appelée. La fonction callback recevra en
paramètre l’objet passé en paramètre de la méthode resolve. Dans notre cas,
il s’agira de la réponse de la requête.
ajax.then(response => console.log("Réponse du serveur :", response));
Si la tache échoue et que c’est la méthode reject qui est appelée, alors
on peut utiliser la méthode catch
pour préciser le traitement à appliquer
lors de l’échec. Cette méthode attend une fonction callback qui recevra en
paramètre le message passé en paramètre de la méthode reject.
ajax.then(response => console.log("Réponse du serveur :", response))
.catch(msg => console.log("Message d'erreur :", msg));
Note
Il est également possible de passer une fonction callback dans le cas
d’un appel à reject en second paramètre de la méthode then
.
ajax.then(response => console.log("Réponse du serveur :", response,
msg => console.log("Message d'erreur :", msg));
Le chaînage des appels¶
Une fonction callback passée en paramètre de la méthode then peut elle-même
produire un résultat. Si le résultat n’est pas un objet de type Promise
alors
un objet de type Promise
est créé pour simplement appeler la fonction
resolve avec la valeur de retour. Ainsi, le modèle des promesses garantie
que l’on peut chaîner les appels à la méthode then.
ajax.then(response => JSON.parse(response))
.then(json => {
if (json.id === undefined) {
throw Error("il manque l'id");
}
return json;
})
.then(json => {
console.log(json);
return json;
})
.catch(msg => console.log("Erreur : ", msg));
Si une des méthodes callback passées aux méthodes then produit une exception, le chaînage des appels est interrompu et c’est la fonction passée à catch qui est exécutée.
L’intérêt des promesses est de fournir un modèle lisible et universel pour la prise en charge des appels asynchrones. S’il est assez simple de créer ses propres promesses, de plus en plus de frameworks et de bibliothèques JavaScript proposent des API qui sont directement basées sur le modèle des promesses.
Traitements parallèles¶
Le modèle des promesses permet facilement de mettre en place des traitements parallèles.
Ainsi, la fonction Promise.all
prend en paramètre un tableau (ou un itérable)
de promesses et retourne une promesse qui s’achève lorsque toutes les promesses
seront achevées.
La fonction Promise.race
prend en paramètre un tableau (ou un itérable)
de promesses et retourne une promesse qui s’achève lorsque la première des promesses
sera achevée.