I. Rappel sur la notion de callback

I-A. Qu'est-ce qu'un callback ?

JavaScript tel qu'il est implémenté actuellement par les différents navigateurs est monothread, c'est-à-dire que le moteur d'exécution du code ne peut effectuer qu'une seule opération à la fois (contre plusieurs en parallèle s'il était multithread). Cela constitue un problème car tant qu'un code JavaScript s'exécute l'interface Web reste bloquée, ce qui provoque une dégradation de l'expérience utilisateur.

Pour contourner ce problème, on utilise généralement les capacités du langage JavaScript à effectuer des opérations asynchrones, c'est-à-dire à libérer le thread de l'interface utilisateur en attendant que certaines conditions soient remplies pour continuer l'exécution du code.

Il est très simple de simuler un appel asynchrone en JavaScript en utilisant les fonctions setTimeout() ou setInterval(). Celles-ci permettent de différer l'exécution d'une fonction selon un intervalle de temps laissé libre au développeur, la différence étant que dans le premier cas, l'exécution n'aura lieu qu'une seule fois et dans le second, l'exécution sera répétée à intervalle régulier.

 
Sélectionnez
// Cette fonction sera exécutée dans 1000 millisecondes
setTimeout(
    function () {
        console.log("Exécution différée d'une seconde.");
    },
    1000
);

L'avantage de ces deux fonctions est que pendant la durée d'attente, l'interface utilisateur peut réagir aux interactions de celui-ci. Pourtant, en tâche de fond, le chronomètre tourne toujours.

Dans cet exemple, la fonction anonyme définie pour s'exécuter lorsque le temps se sera écoulé est un exemple typique de callback : elle ne s'exécute qu'en réaction à la réalisation de certaines conditions (ici en l'occurrence, à la fin du décompte du chronomètre).

I-B. Comment jQuery utilise les callbacks

La librairie jQuery fait usage des callbacks dans deux cas précis :

  • dans l'API d'animation, grâce à laquelle on peut chaîner différentes animations pour que celles-ci s'exécutent les unes à la suite des autres. Dans ce cas, ces animations constituent elles-mêmes des calbacks, puisque leur exécution sera dépendante de la fin de l'animation précédente ;
  • dans l'API Ajax, dont la nature est par définition asynchrone (bien qu'on puisse l'utiliser volontairement en mode synchrone). Il est en effet impossible de connaître à l'avance la durée d'une opération d'appel vers un serveur. On utilise donc des callbacks pour indiquer comment devra réagir le client lorsque le serveur aura répondu.
 
Sélectionnez
/* Exemple d'utilisation asynchrone de l'API d'animation */
// Il y aura un décalage de 800 millisecondes entre les animations "slideUp" et "fadeIn"
$("div#element").slideUp(400).delay(800).fadeIn(400);
 
Sélectionnez
/* Exemple d'utilisation asynchrone de l'API Ajax */
// Les fonctions anonymes définies dans "success" et "error" ne s'exécuteront qu'une fois l'appel Ajax terminé,
// et en fonction de la réussite ou de l'échec de celui-ci
$.ajax({
    "url": "http://odata.netflix.com/Catalog/Titles" +
           "?$filter=substringof('indiana jones',Name)&$format=json&$callback=callback",
    "dataType": 'jsonp',
    "type": 'GET',
    "jsonpCallback": 'callback',
    "success": function (data, textStatus, jqXHR) {
        console.log("L'appel Ajax est une réussite.");
    },
    "error": function (jqXHR, textStatus, errorThrown) {
        console.log("L'appel Ajax est un échec.");
    }
});

Notez qu'il est aussi possible dans le cas de l'API Ajax de définir non pas une seule fonction de callback, mais un tableau de celles-ci, qui seront alors exécutées à tour de rôle.

Ces deux exemples illustrent bien l'intérêt des callbacks dans l'utilisation de jQuery. Toutefois, leur omniprésence a conduit les membres de l'équipe de développement de la célèbre librairie à s'interroger sur la meilleure manière de les représenter. C'est ainsi qu'a été introduite à partir de la version 1.5 toute une API destinée à leur manipulation.

II. Consommer des callbacks dans jQuery

L'arrivée dans jQuery d'une API qui leur est dédiée a permis une refonte complète de l'implémentation interne des callbacks. Les API d'animation et Ajax sont les premières à en bénéficier, et les objets retournés par leurs différentes méthodes ont été augmentés pour les rendre compatibles avec la notion de callback.

Ces objets disposent maintenant d'une méthode promise() qui est le point d'entrée de l'ajout de callbacks. Une promise (« promesse ») est une version réduite de l'objet utilisé par jQuery pour gérer les callbacks, permettant seulement de rattacher ces derniers, sans possibilité de résoudre l'opération sous-jacente. Mais nous y reviendrons.

À partir de cette méthode promise(), il est possible d'empiler nos différents callbacks. Ceux-ci peuvent être de trois types :

  • les callbacks en cas de réussite (« done callbacks ») ;
  • les callbacks en cas d'échec (« fail callbacks ») ;
  • les callbacks lors de la notification de la progression de l'opération (« progress callbacks »). Ceci constitue une nouveauté introduite dans jQuery 1.7.

Les callbacks rattachés à la promise seront désempilés et exécutés dans l'ordre. Les paramètres qui leur seront passés seront dépendants du type de l'opération ainsi que de la réussite ou de l'échec de celle-ci. Par exemple, en cas de réussite d'une requête Ajax, les paramètres passés aux callbacks seront ceux passés traditionnellement à la propriété « success » de l'API Ajax (data, textStatus et jqXHR).

Voyons maintenant comment effectivement rattacher des callbacks à une promise.

II-A. Manipuler une seule promise

Rattacher des callbacks à une promise unique se fait par le biais des méthodes done(), fail() et progress().

 
Sélectionnez
$(document).ready(function () {
    var jQueryObj = $("#element").slideUp().delay(1500).fadeIn();
    jQueryObj.promise()
         .done(function () { console.log("L'animation est réussie."); })
         .fail(function () { console.log("L'animation est ratée."); })
         .progress(function () { console.log("L'animation est en cours de progression."); });
});

Dans cet exemple, jQueryObj est un objet jQuery tout ce qu'il y a de plus traditionnel, tel qu'il est renvoyé par la majorité des méthodes chaînables de jQuery, notamment les méthodes d'animation. Mais cet objet jQuery dispose d'une nouvelle fonction promise() à partir de laquelle il est possible de rattacher des callbacks.

En l'occurrence, si vous exécutez le code, vous verrez que seul le texte « L'animation est réussie. » s'affiche à la fin de l'opération, car les fonctions d'animation ne notifient pas leur progression et ne sont normalement jamais en échec.

Notez que toutes ces méthodes acceptent aussi bien une seule fonction de callback qu'un tableau de celles-ci, auquel cas ces fonctions seront désempilées et exécutées à tour de rôle.

Il existe aussi une autre fonction permettant de rattacher un callback qui sera exécuté dans tous les cas, que l'opération soit une réussite ou un échec. Il s'agit de la fonction always().

 
Sélectionnez
$(document).ready(function () {
    var ajaxCall = $.ajax({
        "url": "http://odata.netflix.com/Catalog/Titles" +
               "?$filter=substringof('indiana jones',Name)&$format=json&$callback=callback",
        "type": 'GET',
        "dataType": 'jsonp',
        "jsonpCallback": 'callback'
    });
			
    ajaxCall
        .always([
            function () { console.log("L'appel Ajax est terminé."); },
            function () { console.log("J'ai déjà dit que l'appel Ajax était terminé."); }
        ]);
});

Dans cet exemple, nous passons à la méthode always() deux fonctions de callback qui seront donc exécutées à tour de rôle, en respectant l'ordre de leur index dans le tableau.

Contrairement à un objet jQuery traditionnel, il n'est pas nécessaire d'appeler la méthode promise() sur un objet jqXHR tel que renvoyé par la méthode jQuery.ajax().

Il existe un piège dans lequel il vous faut prendre garde de ne pas tomber lors de l'utilisation des callbacks avec jQuery.ajax(). Il existe en effet dans cette API une option « beforeSend » qui permet d'altérer l'objet jqXHR avant l'envoi de la requête (pour modifier les en-têtes HTTP par exemple), mais qui permet aussi d'annuler purement et simplement la requête si la fonction de traitement de l'option renvoie false.
Le piège est que lorsque l'on renvoie false, jQuery.ajax() ne renvoie pas une promise mais également false. Si vous utilisez l'option « beforeSend » et cette particularité, il vous faut donc vérifier que la requête a bien été envoyée avant d'essayer de manipuler la promise.
Ceci constitue un comportement un peu gênant qui a été rapporté à l'équipe de développement de jQuery, et qui ne sera vraisemblablement pas corrigé dans un futur proche.

Inconvénient non négligeable : il est impossible de savoir à l'avance quels seront les paramètres passés aux callbacks, puisque ceux-ci seront susceptibles de différer en fonction de la réussite ou de l'échec de l'opération. Il faudra donc en analyser soi-même la nature. La méthode always() se prête donc davantage à des scénarios où les paramètres et l'état final de l'opération importent peu.

Il est bien sûr possible d'obtenir facilement le même résultat que l'utilisation de la méthode always() en passant la même fonction aux méthodes done() et fail() :

 
Sélectionnez
var onComplete = function () {
    console.log("L'appel Ajax est terminé.");
};

ajaxCall
    .done([
        function () { console.log("L'appel Ajax est réussi."); },
        onComplete
    ])
    .fail([
        function () { console.log("L'appel Ajax est raté."); },
        onComplete
    ]);

Il est aussi possible d'utiliser la méthode then(), purement utilitaire, qui permet d'attacher en une seule fois des callbacks de réussite, d'échec et de notification de la progression :

 
Sélectionnez
ajaxCall.then(
    function () { console.log("L'appel Ajax est réussi."); },  // Callback de réussite
    function () { console.log("L'appel Ajax est raté."); },    // Callback d'échec
    function () { console.log("L'appel Ajax est en cours."); } // Callback de progression
);

Tout ce que nous avons vu jusqu'à maintenant, il était déjà possible de l'effectuer sans l'API de callback. Ce qu'apporte essentiellement cette dernière, c'est la possibilité de traiter les callbacks de manière plus globalisée et en particulier, de consolider plusieurs promises pour n'exécuter les callbacks que lorsqu'elles sont toutes résolues. C'est le rôle de la méthode jQuery.when().

II-B. Consolider plusieurs promises avec jQuery.when()

Grâce à jQuery.when(), nous sommes maintenant en mesure de regrouper plusieurs promises afin d'y rattacher des callbacks qui ne seront exécutés que lorsque l'ensemble des opérations sous-jacentes seront résolues.

 
Sélectionnez
// La déclaration des variables jQueryObj et ajaxCall est omise par souci de brièveté
$.when(jQueryObj, ajaxCall)
    .done(function (animParameters, ajaxParameters) {
        console.log("Toutes les opérations se sont terminées normalement.");
    });

jQuery.when() retourne une promise qui regroupe toutes les autres promises passées en paramètre. Nous pouvons ensuite rattacher des callbacks à cette master promise de la même manière qu'expliqué précédemment.

Dans cet exemple, nous regroupons l'opération d'animation et l'opération d'appel Ajax. Le callback de réussite que nous attachons à la nouvelle promise sera exécuté dès lors que ces deux opérations auront été successivement résolues. Par contre, si une seule de ces opérations devait tomber en échec, la promise serait instantanément rejetée.

Autre élément important : les paramètres de résolution sont regroupés dans des tableaux et passés aux callbacks dans le même ordre que les promises sont passées à jQuery.when(). Ainsi dans notre exemple, ajaxParameters sera un tableau contenant tous les paramètres de résolution d'une requête jQuery.ajax(), à savoir les données retournées (ajaxParameters[0]), le statut de la requête (ajaxParameters[1]) et l'objet jqXHR (ajaxParameters[2]).

III. Comment fonctionne l'API de callbacks de jQuery

L'API de manipulation de callbacks de jQuery fonctionne en fait autour de deux API distinctes, l'API jQuery.Callbacks, et l'API jQuery.Deferred.

III-A. L'API jQuery.Callbacks

L'API jQuery.Callbacks permet de créer des listes de fonctions dont l'exécution pourra ensuite être déclenchée au moment opportun.

III-A-1. La manipulation des listes

Une liste de fonctions se crée en appelant le constructeur jQuery.Callbacks(). Celui-ci prend optionnellement une liste de « flags » sur laquelle nous reviendrons bientôt.

Les méthodes add(), remove() et empty() permettent ensuite facilement d'ajouter et supprimer des fonctions à la liste, ou de vider celle-ci.

Notez que contrairement à ce qu'indique la documentation de jQuery, les fonctions de l'API jQuery.Callbacks sont parfaitement chaînables.
En outre, les méthodes d'ajout et de suppression peuvent prendre en paramètre une seule fonction ou un tableau de celles-ci.

 
Sélectionnez
var externalFunction = function () {
    console.log("Fonction nommée.");
};

var listOfCallbacks = $.Callbacks()
    // On ajoute une fonction anonyme et la fonction nommée "externalFunction" à la liste
    .add([
    	function () { console.log("Fonction anonyme."); },
        externalFunction
    ])
    // On retire la fonction nommée "externalFunction" de la liste
    .remove(externalFunction);

Grâce à la fonction has() il est aussi possible de déterminer si une fonction est présente ou non dans une liste.

 
Sélectionnez
// hasFunction contiendra "false" car nous avons retiré la fonction nommée "externalFunction" auparavant
var hasFunction = listOfCallbacks.has(externalFunction);

Une fois notre liste constituée, il est possible d'en exécuter les fonctions à loisir grâce aux méthodes fire() et fireWith().

 
Sélectionnez
var listOfCallbacks = $.Callbacks()
    .add(
        function (text) {
            console.log("Votre texte : " + text);
            console.dir(this);
        }
    )
    // Affichera :
    // "Votre texte : toto"
    // Le dump Firebug de l'objet listOfCallbacks
    .fire("toto")
    // "Votre texte : titi"
    // Le dump Firebug de l'objet window
    .fireWith(window, [ "titi" ]);

La différence est que fire() prend simplement en paramètres les arguments qui seront transmis aux différents callbacks, tandis que fireWith() permet de modifier le contexte d'exécution de ceux-ci, de la même manière qu'on le ferait avec la fonction JavaScript apply() (que jQuery appelle d'ailleurs sous le manteau), c'est-à-dire en passant un nouveau contexte en guise de premier paramètre et un tableau d'arguments en guise de second paramètre.

Accessoirement la méthode fired() permet de savoir si les fonctions de la liste ont été exécutées au moins une fois. Notez que le fait de modifier le contenu de la liste ou même de vider celle-ci entretemps n'y change rien.

 
Sélectionnez
// wasFired contiendra "true" car nous avons exécuté la liste de fonctions au moins une fois, et ce
// bien que nous ayons vidé celle-ci entretemps
listOfCallbacks.empty();
var wasFired = listOfCallbacks.fired();

III-A-2. Le système de flags

Au moment de créer une liste de fonctions avec le constructeur de jQuery.Callbacks(), il est possible de passer à celui-ci une liste de flags sous forme de chaînes de caractères. Ces flags sont de simples mots-clefs qui vont permettre d'altérer le comportement de la liste. Leur usage est cumulatif.

Ces mots-clefs sont actuellement au nombre de quatre :

  • « once » permet de limiter l'exécution de la liste à une seule fois. Les exécutions suivantes seront sans effet ;
  • « unique » permet de s'assurer qu'un callback ne pourra être ajouté qu'une seule fois à la liste. Ajouter un callback à une liste qui le contient déjà sera sans effet et sa position dans la file d'exécution n'en sera pas altérée ;
  • « memory » permet de conserver en mémoire les valeurs passées en paramètre lors de la première exécution de la liste. L'ajout ultérieur de nouveaux callbacks provoquera l'exécution immédiate de ceux-ci avec les valeurs en mémoire. Il est toujours possible d'exécuter à nouveau la liste de fonctions, et donc de modifier la liste des paramètres en mémoire ;
  • « stopOnFalse » permet d'indiquer que l'on souhaite que l'exécution de la liste de fonctions s'interrompe si l'une de celles-ci retourne false.
 
Sélectionnez
/* Exemple d'utilisation du mot-clef "once" */
// Ne s'affichera que "Votre texte est : toto", les exécutions suivantes étant ignorées
$.Callbacks("once")
    .add(function (text) {
        console.log("Votre texte est : " + text);
    })
    .fire("toto")
    .fire("titi")
    .fire("tutu");
 
Sélectionnez
/* Exemple d'utilisation du mot-clef "unique" */
var externalFunction = function (text) {
    console.log("Votre texte est : " + text);
};

// Affichera "Votre texte est : toto" une seule fois car les ajouts multiples d'une même fonction sont ignorés
$.Callbacks("unique")
    .add(externalFunction)
    .add(externalFunction)
    .fire("toto");
 
Sélectionnez
/* Exemple d'utilisation du mot-clef "memory" */
// Affichera "Votre texte est : toto" au moment de l'appel à fire()
// Puis "Vous avez saisi : toto" tout de suite après l'ajout du nouveau callback
$.Callbacks("memory")
    .add(function (text) {
        console.log("Votre texte est : " + text);
    })
    .fire("toto")
    .add(function (text) {
        console.log("Vous avez saisi : " + text);
    });
 
Sélectionnez
/* Exemple d'utilisation du mot-clef "stopOnFalse" */
// N'affichera pas "Votre texte est : toto" car le premier callback retourne "false", interrompant ainsi
// l'exécution du reste de la liste
$.Callbacks("stopOnFalse")
    .add([
        function () {
            console.log("J'interromps l'exécution de la liste");
            return false;
        },
        function (text) {
            console.log("Votre texte est : " + text);
        }
    ])
    .fire("toto");

Comme on le voit, l'éventail des possibilités offertes par ces mots-clefs permet déjà des usages assez variés. Par ailleurs, comme déjà expliqué, il est possible de cumuler l'effet de ces différents mots-clefs, afin d'affiner encore davantage la manière dont pourra être manipulée une liste.

 
Sélectionnez
/* Exemple d'utilisation cumulée des mots-clefs "once" et "memory" */
// Affichera "Votre texte est : toto"
// puis "Vous avez saisi : toto" dès l'ajout du second callback
$.Callbacks("once memory")
    .add(function (text) {
        console.log("Votre texte est : " + text);
    })
    .fire("toto")
    .fire("titi")
    .add(function (text) {
        console.log("Vous avez saisi : " + text);
    });

Dans cet exemple, le deuxième appel à fire() est inhibé par le mot-clef « once », mais l'utilisation du mot-clef « memory » permet malgré tout d'exécuter instantanément les callbacks ajoutés ultérieurement. Par contre, le cumul de ces deux mots-clefs a aussi pour effet de geler une bonne fois pour toutes les valeurs des paramètres enregistrées en mémoire. Ainsi, comme on le voit ici, c'est bien la valeur « toto » qui est retenue lors de l'ajout du second callback, le seconde exécution avec la valeur « titi » ayant été totalement ignorée.

Vous aurez remarqué que dans l'API jQuery.Callbacks, il n'est pas question de « promise » ou de différents types de callbacks de réussite ou d'échec. En effet, cette API se contente de jouer un rôle bas niveau, en proposant le strict minimum pour gérer des listes de fonctions. L'API haut niveau n'est autre que jQuery.Deferred.

III-B. L'API jQuery.Deferred

L'API jQuery.Deferred repose sur l'API jQuery.Callbacks. C'est ici que nous allons retrouver notre notion de « promise », ainsi que nos différents types de callbacks. Pour gérer ces derniers, l'API jQuery.Deferred utilise des objets jQuery.Callbacks avec les flags « once » et « memory » (sauf pour la notification de progression, pour laquelle il n'utilise que « memory »).

jQuery.Deferred est l'API de choix pour gérer des listes de callbacks sur des opérations asynchrones et c'est justement elle qu'utilise jQuery dans les API d'animation et Ajax. Lorsque l'on manipule ces dernières, on n'a accès qu'à des promises, ne permettant que de rattacher des callbacks. Mais nous allons voir maintenant tout l'éventail de possibilités que permet jQuery.Deferred.

III-B-1. Manipulation d'un objet jQuery.Deferred

Comme nous l'avons déjà dit, une promise ne permet que de rattacher des callbacks à un objet qui a seul le contrôle de l'exécution de ceux-ci. Ce fameux objet est en fait l'objet jQuery.Deferred, qui va donc décider du moment où se fera cette exécution et de la manière dont elle se fera.

Lorsque l'on manipule un objet jQuery.Deferred, le principe est donc de n'exposer que sa promise afin de rester maître du déroulement asynchrone des opérations.

 
Sélectionnez
/* Exemple d'utilisation d'un objet jQuery.Deferred */
// On appelle une fonction qui renvoie une promise
operation().done(function (magicNumber) {
    console.log("Le nombre magique est " + magicNumber);
});

function operation() {
    var dfd = $.Deferred();
    
    // L'objet jQuery.Deferred ne sera résolu qu'au bout de 5 secondes
    setTimeout(function () {
        dfd.resolve(42);
    }, 5000);
    
    return dfd.promise();
}

On voit dans cet exemple que la fonction operation() agit comme une boîte noire. Le code appelant ne dispose que d'une promise, et ne sait pas quand et avec quels paramètres celle-ci sera résolue. Cette promise est renvoyée grâce à la méthode promise() de l'objet jQuery.Deferred, dont le constructeur peut prendre optionnellement deux paramètres : une chaîne de caractères indiquant à quelle queue la promise est liée (par exemple la queue « fx » pour les méthodes de l'API d'animation) et un objet auquel on souhaite rattacher la promise, comme le fait l'API Ajax avec l'objet jqXHR.

Notez que vous pouvez tout à fait choisir d'exposer l'intégralité de l'objet jQuery.Deferred, déléguant ainsi le choix de l'exécution des callbacks, mais ce n'est pas vraiment le principe souhaité d'utilisation, et l'API jQuery.Callbacks convient mieux dans ce cas.

Il existe trois méthodes permettant de notifier l'état d'avancement d'un objet jQuery.Deferred :

  • resolve() qui permet de résoudre l'objet ;
  • reject() qui permet de rejeter l'objet ;
  • notify() qui permet d'indiquer l'avancement de l'opération. Ceci constitue une nouveauté introduite dans jQuery 1.7.
 
Sélectionnez
/* Exemple d'utilisation des fonctions de résolution de l'objet jQuery.Deferred */
operation().then(
    // Réussite
    function (response) {
        console.log("L'opération est terminée avec succès et la réponse est : " + response);
    },
    // Échec
    function (error) {
        console.log("L'opération a raté à cause de l'erreur : " + error);
    },
    // Avancement
    function (progress) {
        console.log("L'opération en est actuellement à l'étape : " + progress);
    }
);

function operation() {
    var dfd = $.Deferred();
    var zeroOrOne = Math.round(Math.random());
    
    setTimeout(function () {
        dfd.notify("Je m'échauffe");
    }, 1000);
    
    setTimeout(function () {
        dfd.notify("Prêt à l'action");
    }, 2000);
    
    setTimeout(function () {
        if (zeroOrOne === 1)
            dfd.resolve("Le chiffre un est tombé");
        else
            dfd.reject("Le chiffre zéro est tombé");
    }, 3000);
    
    return dfd.promise();
}
Image non disponible

Dans cet exemple, nous utilisons une nouvelle fois la fonction setTimeout() native du langage JavaScript pour simuler une opération asynchrone. Ainsi, au bout d'une seconde la fonction operation() va notifier une première fois les callbacks de progression attachés à sa promise pour leur indiquer qu'elle « s'échauffe », puis au bout d'une deuxième seconde, indiquer qu'elle est « prête à l'action », et enfin au bout d'une troisième seconde, résoudre ou rejeter l'objet jQuery.Deferred en fonction d'une valeur entre 0 et 1 tirée au sort aléatoirement avec la fonction native Math.random(). La capture d'écran montre le résultat de ces résolutions successives de callbacks dans la console de Firebug.

Notez que ces trois méthodes existent aussi dans une version alternative permettant d'altérer le contexte d'exécution des callbacks qui s'y rattacheraient : resolveWith(), rejectWith() et notifyWith(). Comme pour la fonction fireWith() de l'API jQuery.Callbacks, celles-ci utilisent sous le manteau la fonction JavaScript apply().

Celui qui contrôle l'objet jQuery.Deferred est seul maître de son état. Au moment de la création d'un objet, celui-ci sera toujours pending (« en attente »), c'est-à-dire qu'il n'y a encore eu ni résolution ni rejet. Il est possible de contrôler l'état d'un objet jQuery.Deferred grâce à la méthode state() qui renvoie une chaîne de caractères pouvant prendre les valeurs pending, resolved ou rejected. La promise a également accès à cette méthode et à ces informations.

Nous avons déjà dit que pour gérer les callbacks, jQuery.Deferred reposait sur l'utilisation de jQuery.Callbacks avec les flags « once » et « memory », et par ailleurs, nous avons étudié plus tôt le résultat de l'utilisation conjointe de ceux-ci. Nous pouvons donc en déduire deux affirmations :

  • une fois qu'un objet jQuery.Deferred a été transitionné dans l'état resolved ou rejected, son état ne peut plus changer, et les callbacks ajoutés ultérieurement aux méthodes done() ou fail() de sa promise seront exécutés immédiatement. Par contre, la notification de progression avec notify() peut être effectuée plusieurs fois tant que l'objet est dans l'état pending ;
  • les paramètres utilisés lors de la résolution ou du rejet d'un objet jQuery.Deferred ne peuvent plus être modifiés après coup. Ceux qui sont passés au moment de la transition de l'état de l'objet jQuery.Deferred seront conservés en mémoire et passés en paramètre de tout callback qui pourrait être attaché ultérieurement. Là encore, la notification de progression fait exception car il est possible de modifier les paramètres d'exécution des callbacks qui s'y rattachent.
 
Sélectionnez
/* Exemple de comportement de jQuery.Deferred */
// Affichera "L'opération est ratée."
operation().then(
    // Réussite
    function (response) {
        console.log("L'opération est réussie.");
    },
    // Échec
    function (error) {
        console.log("L'opération est ratée.");
    }
);

function operation() {
    var dfd = $.Deferred();
    
    // Exécution immédiate de l'objet jQuery.Deferred
    dfd.reject();
    
    // Ne sert à rien car l'objet a déjà été rejeté
    dfd.resolve();
    
    return dfd.promise();
}

Il y a deux choses à noter dans ce dernier exemple. Tout d'abord, l'objet jQuery.Deferred est rejeté tout de suite après sa création, et les callbacks qui sont attachés à sa promise, même si cette opération n'a lieu qu'après le rejet, sont donc exécutés instantanément. Par ailleurs, la résolution qui a lieu dans un second temps est totalement ignorée, ce qui signifie que l'état de l'objet jQuery.Deferred n'est pas altéré et que les callbacks de résolution attachés à sa promise ne seront pas exécutés.

Ce sont toutes ces règles respectant le design CommonJS Promises/A qui permettent d'avoir une API jQuery.Deferred à la fois souple et robuste.

III-B-2. Filtrer et chaîner les callbacks avec la fonction pipe()

Une autre méthode utilitaire de l'API jQuery.Deferred qui mérite que l'on s'y attarde est la méthode pipe(), qui permet de filtrer les paramètres passés aux callbacks lors de la résolution, le rejet, ou la notification de la promise. Cette méthode est accessible aussi bien par l'objet maître jQuery.Deferred que par ses promises.

 
Sélectionnez
/* Exemple de filtrage avec pipe() */
// Affichera "L'OPÉRATION EST EN COURS."
// Affichera "Est-ce que le tableau est vide : false"
operation()
    .done(function (arrayIsEmpty) {
        console.log("Est-ce que le tableau est vide : " + arrayIsEmpty);
    })
    .progress(function (comment) {
        console.log(comment);
    });

function operation() {
    var dfd = $.Deferred();
    
    setTimeout(function () {
        dfd.notify("L'opération est en cours.");
    }, 1000);
    
    setTimeout(function () {
        dfd.resolve([ "toto" ]);
    }, 2000);
    
    return dfd.pipe(
    	// Filtrage des paramètres de résolution
        function (array) {
            return array.length === 0;
        },
        // Pas de filtrage sur les paramètres de rejet
        null,
        // Filtrage des paramètres de notification de progression
        function (comment) {
            return comment.toUpperCase();
        }
    );
}

Dans cet exemple la méthode pipe() filtre les paramètres des callbacks de résolution et de progression. En passant « null » à pipe() en guise de fonction filtrant les paramètres de rejet, nous indiquons que nous voulons que ceux-ci ne soient pas altérés.

Le paramètre de la notification de progression est une chaîne de caractères, qui grâce à la fonction pipe() sera passée en majuscules avant d'être passée aux callbacks rattachés à la promise. Le paramètre de la résolution est théoriquement un tableau, mais nous l'interceptons pour renvoyer à la place un booléen indiquant si celui-ci est vide.

Notez bien encore une fois que ces filtrages peuvent aussi bien être appliqués au niveau de l'objet jQuery.Deferred lui-même qu'au niveau de ses promises, et qu'en outre, la méthode pipe() renvoie elle-même une nouvelle promise, ce que démontre là aussi notre exemple.

Mais la méthode pipe() a une autre utilité. Elle permet en effet de chaîner les appels asynchrones, afin de n'obtenir que le résultat final.

 
Sélectionnez
/* Exemple de chaînage avec pipe() */
// Affichera "Les données ont été chargées."
// Affichera "L'opération a réussi."
operation()
    .done(function () {
        console.log("L'opération a réussi.");
    })
    .progress(function (status) {
        console.log(status);
    });

function operation() {
    var dfd = $.Deferred();
    
    var promiseAjax = $.ajax({
        "url": "http://odata.netflix.com/Catalog/Titles" +
               "?$filter=substringof('macross',Name)&$format=json&$callback=callback",
        "dataType": 'jsonp',
        "type": 'GET',
        "jsonpCallback": 'callback'
    });
    
    promiseAjax.done(function () {
        dfd.notify("Les données ont été chargées.");
    });
    
    var promiseAnimation = promiseAjax.pipe(function (data) {
        var list = $("ul#list");
        $.each(data.d.results, function (i, item) {
            $("<div>").text(item.Name).appendTo(list);
        });
        dfd.resolve();
        return list.delay(1000).slideDown().promise();
    });
    
    return $.when(dfd, promiseAnimation);
}

Il se passe beaucoup de choses dans cet exemple. Nous chargeons des données grâce à un appel Ajax asynchrone, mais puisque nous ne contrôlons pas celui-ci, nous créons un autre objet jQuery.Deferred pour notifier le code appelant du moment où les données ont été effectivement chargées. Par ailleurs, nous chaînons grâce à la méthode pipe() la promise de cet appel Ajax avec une routine qui va charger le résultat dans une liste à puces et animer l'affichage de celle-ci. Normalement la méthode pipe() retourne une nouvelle promise prenant en compte le filtrage que nous avons appliqué. Mais ici nous indiquons que nous souhaitons retourner la promise renvoyée par la file d'animation.

Finalement, nous renvoyons au code appelant notre objet jQuery.Deferred consolidé avec la promise de la file d'animation. Cette astuce nous permet à la fois de notifier le code appelant car nous avons la main sur l'objet maître jQuery.Deferred, mais aussi de ne résoudre l'ensemble de l'opération que lorsque l'animation d'affichage est terminée, alors qu'à ce stade l'objet maître a lui déjà été résolu.

IV. Conclusion

Le monde du Web est en pleine effervescence, car un nouveau modèle d'application Web commence à prendre beaucoup de place sur le devant de la scène : les single page applications, dans lesquelles une seule page web gère l'ensemble d'un site. Ce modèle repose sur des interactions entre la page et le serveur sous forme d'appels Ajax (ou bientôt WebSockets), mais aussi sur de nombreuses animations pour remplacer la navigation de page en page. Cela implique que les fonctions asynchrones vont prendre une place centrale dans l'écosystème du Web, rendant ainsi la compréhension des callbacks absolument indispensable pour tout développeur Web qui se respecte. L'API de jQuery, par sa richesse et sa souplesse, sera alors un allié idéal pour parvenir à concevoir des applications offrant une expérience utilisateur de premier ordre.

IV-A. Pour aller plus loin

La documentation de jQuery sur les API Callbacks et Deferred :

IV-B. Remerciements

Merci à _tom_ et à jacques_jean pour leurs corrections et leur relecture attentive, et à Bovino et vermine pour l'encadrement éditorial.