Il y a 11 ans -
Temps de lecture 7 minutes
Les objets différés et les promesses en jQuery
En quelques années, jQuery s’est répandu de manière fulgurante, et aujourd’hui, rares sont les projets webs n’ayant pas une version de la librairie dans leurs dépendances. Pourtant, certaines fonctionnalités restent méconnues, telles que les objets différés.
Introduit en version 1.5, les objets différés ont pour objectif de faciliter l’écriture de méthodes synchrones ou asynchrones en séparant clairement l’appel de la résolution de l’appel. Cette séparation permet, entre autres, de simplifier l’écriture d’appels asynchrones successifs ou simultanés.
$.Deferred
Les objets différés se construisent à l’aide de la fonction jquery $.Deferred. Ces objets exécutent des callbacks en fonction de leurs états courants, ces derniers pouvant être :
- « resolved » représentant un appel réussi ;
- « rejected » représentant un appel échoué ;
- « pending » représentant un état non fini (ni résolu, ni rejeté).
Pour exécuter les callbacks, il suffit d’appeler une des deux méthodes « resolve » ou « reject ».
La méthode « resolve » lancera l’exécution des callbacks « done » et « always ».
La méthode « reject » lancera l’exécution des callbacks « fail » et « always ».
Exemple (voir cet exemple sur JsFiddle):
var deferred = $.Deferred(); deferred.done(function(value) { alert('succes '+value); }); deferred.fail(function(value) { alert('fail '+value); }); deferred.resolve('test'); deferred.reject('rejected ?');
Cet exemple montre que seule la callback « done » est exécutée. Une fois dans un état fini, on ne peut plus changer l’état d’un objet différé, ce qui explique que la callback « fail » ne soit pas exécutée.
Par ailleurs, il est tout a fait possible d’ajouter des callbacks après l’appel à « resolve » ou « reject », et celles-ci seront exécutées de la même façon.
Exemple (voir cet exemple sur JsFiddle):
var deferred = $.Deferred(); deferred.resolve('test'); deferred.done(function(value) { alert('succes '+value); });
Dans l’exemple ci-dessus, la callback « done » est effectivement appelée, bien que l’état de l’objet différé soit déjà à « resolved ».
Notification
Les objets différés sont avant tout destinés aux traitements de code asynchrone. Il peut donc être utile de monitorer la progression de l’exécution de ce code. Cela peut se faire via la méthode « notify » et la callback « progress ». À chaque appel de « notify », « progress » est exécutée.
Exemple (voir sur JsFiddle):
var deferred = $.Deferred(); deferred.progress(function(value) { alert('state '+value); }); deferred.done(function(value) { alert(value); }); var i = 0; var interval = setInterval(function() { if(i < 5) { deferred.notify(i); i = i+1; } else { deferred.resolve('done !!'); clearInterval(interval); deferred.notify('after end'); } },1000);
Le code ci-dessus notify 5 fois l’objet différé, avant de le résoudre. On constate donc que « notify », contrairement à « resolve » et « reject », peut être appelée plusieurs fois de suite tant que l’objet est en état pending, mais qu’une fois l’objet différé dans un état terminal, la notification n’a plus d’effet.
Les promesses
Les promesses jQuery sont des objets ayant une interface très proche des objets différés, mais sur lesquels on ne peut appeler directement les méthodes « resolve », « reject » et « notify ». Les méthodes jQuery effectuant des appels ajax ($.ajax, $.get, $.post, $.getJSON, etc.) renvoient toutes des promesses, qui sont donc exploitables de la même façon qu’un objet différé.
Exemple (voir sur JsFiddle) :
var promiseOfPerson = $.post('/echo/json/',{ delay:1, json:JSON.stringify({name:'test'}) }); promiseOfPerson.done(function(person) { alert("Name: "+person.name); });
On voit dans cet exemple que l’utilisation des promesses a permis un découpage propre entre l’appel aux données et le traitement de ces données.
Le chaînage
Les objets différés, ainsi que les promesses, possèdent une méthode pipe, qui permet de chainer les appels. Dans son cas d’utilisation le plus simple, pipe va permettre de transformer les données reçues par les callbacks.
Exemple (voir sur JsFiddle):
var promiseOfPerson = $.post('/echo/json/',{ delay:1, json:JSON.stringify({name:'test'}) }); var promiseOfName = promiseOfPerson.pipe(function(person) { return person.name; }); promiseOfPerson.done(function(person) { alert("Received a person: "+person.name); }); promiseOfName.done(function(name) { alert("Received a name: "+name); });
Mais la méthode pipe permet d’aller plus loin, lorsque que la fonction qui lui est passée renvoie un objet différé. De cette façon, on peut ainsi chaîner les appels asynchrones. Si un seul des appels échoue, alors toute la chaîne est rejetée.
Exemple (voir sur JsFiddle):
//Renvoie une promesse qu'un identifiant correspondant au nom sera renvoyé function getUserIdByName(name) { return $.post('/echo/json/',{ delay:1, json:JSON.stringify(2) }); } //Renvoie une promesse qu'un solde correspondant à l'identifiant utilisateur sera renvoyé function getAccountBalanceByUserId(idUser) { return $.post('/echo/json/',{ delay:1, json:JSON.stringify(1800) }); } //Renvoie une promesse qu'un solde correspondant au nom sera renvoyé function getAccountBalanceByUserName(name) { return getUserIdByName(name).pipe(getAccountBalanceByUserId); } var promiseOfBalance = getAccountBalanceByUserName('Georges'); promiseOfBalance.done(function(balance) { alert('Balance: '+balance); });
Le cas ci-dessus montre que l’on peut chaîner 2 appels Ajax simples afin de retourner une valeur plus complexe dans une troisième fonction. Ce mode d’appel permet d’éviter les callbacks dans des callbacks dans des callbacks, et ainsi de simplifier la lecture et l’écriture du code.
Le parallélisme
JQuery fournit la méthode $.when permettant de paralléliser les appels asynchrones et de ne jouer une callback qu’une fois tous les appels effectués. On arrive ainsi à un équivalent simplifié de Fork/Join.
Exemple (voir le jsFiddle – le webservice echo renvoit l’objet json passé en post)
function getNameById(id) { return $.post('/echo/json/',{ delay:id, json:JSON.stringify('name'+id) }); } $.when(getNameById(1), getNameById(2)).then(function(ajaxArgs1, ajaxArgs2) { // ajaxArgs contient [jsonResult, "success", statusText, jqXHR ] alert(ajaxArgs1[0]+' '+ajaxArgs2[0]); });
Dans l’exemple ci-dessus, la boite d’alerte ne sera affichée que lorsque les 2 requêtes ajax seront résolues.
On peut dès lors imaginer un cas plus complexe :
Supposons que nous ayons une liste d’identifiants, dont le nombre peut varier, et que l’on veut récupérer le nom correspondant à chacun de ces identifiants, de façon synchrone.
Les promesses permettent de le faire en parallèle, sans condition de blocage complexe :
Exemple : (voir le JsFiddle)
//Génération d'une liste d'identifiants de taille aléatoire var listOfIds = []; var nbOfId = 1 + Math.random() * 5; for(var i =1; i < nbOfId; ++i) { listOfIds.push(i); } alert('Les ids à récupérer sont ['+listOfIds+']'); //Fonction retournant une promesse de nom correspondant à l'identifiant passé en paramètre function getNameById(id) { return $.post('/echo/json/',{ delay:id, json:JSON.stringify('name'+id) }); } //On transforme la liste d'identifiants en liste de promesses de noms correspondant à ces id var listOfPromises = listOfIds.map(getNameById); // La fonction when ne prend pas de tableau en entrée, mais un varargs. // Il est donc nécessaire de passer par la fonction apply pour invoquer when, // afin de transformer le tableau de promesses en varargs $.when.apply($, listOfPromises).then(function() { var listOfName = []; for(var i = 0; i < listOfPromises.length; ++i) { //Arguments est une variable magique contenant les paramètres de la fonction //Les paramètres sont passés dans le même ordre que les promesses. listOfName.push(arguments[i][0]); } alert('La liste des noms est : ['+listOfName+']'); });
Et si l’implémentation ci-dessus peut sembler complexe, c’est principalement lié au langage javascript lui-même. En effet, la même fonctionnalité en coffeescript est bien plus courte et lisible :
#Génération d'une liste d'identifiants de taille aléatoire nbOfId = 1 + Math.random() * 5 listOfIds = (i for i in [1..nbOfId]) alert("Les ids à récupérer sont [#{listOfIds}]"); #Fonction retournant une promesse de nom correspondant à l'identifiant passé en paramètre getNameById = (id) -> $.post('/echo/json/',{ delay:id json:JSON.stringify('name'+id) }) #On transforme la liste d'identifiant en liste de promesses de noms correspondant à ces id listOfPromises = (getNameById(id) for id in listOfIds) #Les ... permettent de transformer un tableau en varargs # et vice-versa en fonction du contexte $.when(listOfPromises...).then (ajaxArgs...) -> listOfName = (arg[0] for arg in ajaxArgs) alert("La liste des noms est : [#{listOfName}]");
En résumé
Les objets différés et les promesses sont d’ores et déjà techniquement utilisables sur la plupart des projets webs, et leur capacité à faciliter la division du code en briques simples en font des outils dont il serait dommage de se passer.
Commentaire
4 réponses pour " Les objets différés et les promesses en jQuery "
Published by Jocelyn Lecomte , Il y a 11 ans
Merci pour cet article intéressant, bien qu’un peu court vu la complexité du sujet.
J’ai relevé quelques petites coquilles:
– Au début de l’article: « La méthode « fail » lancera l’exécution des callbacks « fail » et « always ». » –> C’est la méthode reject et pas la méthode fail
– Le lien vers la 1ere fiddle sur le chainage est mauvais
Published by blemoine , Il y a 11 ans
Merci pour le retour, les coquilles sont corrigées.
L’article se veut effectivement plus une introduction qu’un essai exhaustif sur le sujet. L’idée derrière était plus de montrer aux gens que la fonctionnalité existe, et qu’elle ne demande qu’à être utilisée.
Published by Visite guidée de Bordeaux (partie 2) | , Il y a 7 ans
[…] différés et les promesses. Ce mécanisme est bien expliqué dans de nombreux articles, dont celui-ci. Le plugin permet, à partir de sa fonction de rappel que l’on peut modifier dans le fichier […]
Published by jap , Il y a 7 ans
Merci pour cet article très bien rédigé qui permet de comprendre un peu mieux un sujet pas forcément facile à appréhender.