Les promesses en AngularJS (3/3)

Troisième et dernier article de cette présentation, nous allons à présent rentrer dans le cœur du sujet et certainement ce qui vous intéresse le plus : au final, comment fonctionne une promesse en interne ? Comment fait-on pour créer soi-même une promesse ? Pour enchaîner / combiner des promesses entre elle ? Tout cela sera couvert dans ce dernier article.

Créer une promesse maison avec $q

Jusqu’à présent, nous avons vu le fonctionnement des promesses et leur utilisation dans les services fournis par AngularJS. Nous allons à présent voir comment créer nos propres promesses. Pour cela, AngularJS s’appuie sur $q qui est une implémentation de la librarie Q en Javascript. Voici un exemple :

var getPromise = function(condition) {
    var deferred = $q.defer();

    if(condition) {
        deferred.resolve("Success");
    } else {
        deferred.reject("Error");
    }

    return deferred.promise;
}

Plusieurs choses se passent ici. Dans un premier temps, on crée un objet « deferred ». Ce type d’objet contient 4 éléments : 3 fonctions et un autre objet. Les trois fonctions sont les suivantes : resolve(value), reject(reason) et notify(value). Le quatrième élément est un objet de type « promise » qui va justement nous permettre de renvoyer la promesse.

Une fois l’objet deferred créé, on peut alors soit résoudre la promesse par l’appel de la fonction resolve(value), soit la rejeter avec reject(reason). Nous ne parlerons toujours pas de notify(value) pour l’instant. On retourne ensuite la promesse via deferred.promise afin que notre fonction soit « thenable » c’est à dire que l’on puisse enchaîner l’appel de notre fonction par then() et y greffer des callbacks de succès et d’erreur. De manière générale, le fait de retourner la promesse vous donne accès à toutes les fonctions de l’API des promesses : then(successCallback, errorCallback, notifyCallback) que nous connaissons bien maintenant mais aussi catch(errorCallback) qui est un raccourci pour then(null, errorCallback), ou encore finally(callback, notifyCallback) qui est toujours appelé, que la promesse soit résolue ou rejetée.

Créer une chaîne de promesses

En prenant comme exemple la fonction getPromise() définie ci-dessus, on pourrait donc imaginer ce genre de choses :

getPromise(true).then(function(value) {
    return getPromise(true);
}).then(function(value) {
    return getPromise(true);
});

Résultat : Les promesses sont appelées à la suite et tout le monde est en succès.

getPromise(true).then(function(value) {
    return getPromise(false); // Rejected

}).then(function(value) {
    return getPromise(true); // Never called

});

Résultat : La première promesse est résolue. Du coup on tombe dans le premier then(), le second appel à getPromise() est quant à lui rejeté, du coup on saute le then() suivant car seul le callback de succès est défini. Le troisième et dernier appel à getPromise() n’est donc jamais appelé.

getPromise(false).then(function(value) {
    return getPromise(true); // Never called

}).catch(function(value) {
    // errorCallback

}).finally(function() {
    // callback

});

Résultat : La première promesse est rejetée, le callback de succès n’est donc pas appelé. On passe directement au catch() qui va pouvoir traiter le résultat en erreur de la première promesse. Enfin, le finally() est appelé. Si toutes les promesses avaient été résolues en succès c’est le catch() qui n’aurait pas été appelé et on serait passé directement au finally(). Notez aussi que le callback de la fonction finally() ne prend pas d’arguments en paramètre comme le fait then().

Si vous souhaitez tester tout cela vous-même, je vous invite à consulter l’exemple ci-joint dans lequel la fonction getPromise() est définie et accompagnée d’un exemple fonctionnel.

Combiner plusieurs promises en une seule

Quelque chose que l’on n’a pas encore évoqué jusqu’à présent : comment faire pour traiter un ensemble de promesses comme un seul et même traitement et réagir en fonction du résultat de cet ensemble et non indépendamment sur chaque promesse ? Pour cela c’est très simple, il y a $q.all([promises]). Voici un exemple :

$q.all([getPromise(true), getPromise(false)]).then(function(value) {
    // successCallback

}, function(reason) {
    // errorCallback

});

$q.all() retourne en réalité une nouvelle promesse créée en interne dont le résultat dépend du succès unanime de toutes les promesses envoyées. En clair, si une seule des promesses envoyées est rejetée, alors la promesse globale est elle-même immédiatement rejetée avec comme argument le message d’erreur de la promesse rejetée sans prendre en compte le résultat des autres promesses encore en cours d’exécution. Si aucune promesse n’est rejetée, alors la promesse globale est résolue avec un tableau contenant tous les messages de succès. Pour résumer : la durée de cette promesse globale est donc égale à la promesse en échec la plus rapide ou à la promesse en succès la plus longue.

Vous trouverez un exemple d’utilisation à cette adresse. Cet article approche à sa fin, mais il nous reste encore quelques fonctions bien utiles à couvrir…

Quelques méthodes utiles : when(), notify()

La méthode when(value) permet de « wrapper » la valeur fournie dans une promesse afin de bénéficier de l’API et de pouvoir enchaîner sur d’autres promesses à l’aide de then() par exemple. Cela peut s’avérer utile lorsque le point d’entrée de la chaîne de promesses n’est justement pas une promesse : une fonction d’une librairie tierce ou tout simplement une valeur.

$q.when(5).then(function(value) {
    console.log(value); // 5
    
});

La fonction notify() quant à elle permet de notifier l’appelant de l’avancée d’une promesse. C’est un besoin assez marginal car généralement les promesses sont assez courtes et les promesses trop complexes sont souvent découpées. Néanmoins cela peut s’avérer intéressant si on veut faire une barre de progression et que notre traitement a des jalons facilement identifiables pour notifier l’appelant. Exemple basique avec $timeout.

Pour un exemple plus avancé de notify() (appels récursifs, notifications avec des barres de progression Bootstrap, etc.) je vous invite vivement à jeter un œil à cet exemple sur Plunker.

Je pense que nous avons fait le tour des fonctionnalités des promesses en AngularJS 1.3. Si vous souhaitez revenir sur l’intégralité des exemples couverts par cet article vous pouvez consulter mon JSFiddle. Enfin, n’hésitez surtout pas à me donner votre feedback dans les commentaires.