Tout sur les promesses (pour les applications du Windows Store écrites en JavaScript)

Lorsque vous développez des applications du Windows Store en JavaScript, vous rencontrez des constructions appelées promesses dès que vous réalisez une action impliquant une API asynchrone. En très peu de temps, vous pouvez écrire facilement des chaînes de promesses pour les opérations séquentielles asynchrones.

Au cours de votre travail de développement, vous rencontrerez cependant d'autres usages des promesses, dont les principes de fonctionnement peuvent paraître moins évidents. L'optimisation des fonctions de rendu des éléments d'un contrôle ListView est un bon exemple, comme le montre l'exemple d'optimisation des performances de contrôle HTML ListView. Nous examinerons cet exemple plus en détail dans un prochain billet. Vous pouvez également découvrir cette fabuleuse démonstration (dans une version légèrement modifiée) présentée par Josh Williams lors de son intervention à la conférence //build 2012, et intitulée Deep Dive into WinJS :

 list.reduce(function callback (prev, item, i) {
    var result = doOperationAsync(item);
    return WinJS.Promise.join({ prev: prev, result: result}).then(function (v) {
        console.log(i + ", item: " + item+ ", " + v.result);
    });
})

Cet extrait de code rejoint les promesses d'opérations asynchrones parallèles et fournit leur résultat de façon séquentielle, selon leur ordre d'apparition dans l'élément list. Si en examinant ce code, vous comprenez immédiatement son fonctionnement, vous pouvez passer ce billet ! Dans le cas contraire, examinons ensemble comment fonctionnent réellement les promesses et comment elles sont exprimées dans WinJS (la bibliothèque Windows pour JavaScript), afin de mieux comprendre les types de schémas mis en jeu.

Une promesse ? De quoi s'agit-il exactement ? Les relations de promesse

Commençons par établir une vérité fondamentale : une promesse n'est rien d'autre qu'une construction de code ou, si vous préférez, une convention d'appel. En tant que telles, les promesses n'ont aucune relation intrinsèque avec les opérations asynchrones, elles sont simplement très utiles dans ce contexte ! Une promesse est tout simplement un objet représentant une valeur pouvant être disponible à tel ou tel moment dans le futur, mais qui peut également être déjà disponible. À cet égard, la promesse est semblable à sa définition usuelle dans le cadre des relations humaines. Si je dis à quelqu'un « Je te promets que je vais t'apporter des croissants », il est parfaitement clair que je ne possède pas encore les croissants, mais que je suppose que je les aurai à un moment ou à un autre. Une fois que je les ai en ma possession, je les apporte à la personne en question.

Ainsi, une promesse implique une relation entre deux agents : l'initiateur, qui fait la promesse d'apporter telle ou telle marchandise, et le consommateur, qui est à la fois le destinataire de la promesse et des marchandises. Peu importe la manière dont l'initiateur parvient à obtenir les marchandises, cela ne concerne pas le destinataire. De même, le consommateur peut faire ce que bon lui semble de la promesse elle-même et des marchandises livrées. Il peut même partager les marchandises obtenues avec d'autres consommateurs.

La relation entre l'initiateur et le consommateur se compose de deux étapes : la création et la réalisation. Tous ces éléments sont indiqués sur le schéma ci-dessous.

promise_diagram

Le fait que la relation se compose de deux étapes explique pourquoi les promesses fonctionnent parfaitement avec une livraison asynchrone, comme le montre le déroulement du flux sur le schéma. Une fois que le consommateur reçoit l'accusé de réception de la demande (la promesse), il peut « faire sa vie » (de façon asynchrone) au lieu d'attendre (de façon synchrone). Par conséquent, le consommateur peut faire autre chose pendant qu'il attend que la promesse soit réalisée. Il peut par exemple répondre à d'autres demandes, ce qui est justement le but principal des API asynchrones. Que se passe-t-il si les marchandises sont déjà disponibles ? La promesse est réalisée immédiatement, ce qui transforme l'ensemble du processus en une sorte de convention d'appel synchrone.

Bien évidemment, cette relation est un peu plus complexe que cela. Au cours de votre vie, vous avez certainement déjà fait des promesses, et des personnes vous ont également fait des promesses. Bien que la plupart de ces promesses aient été réalisées, en pratique de nombreuses promesses ne sont pas tenues : le livreur de pizzas, par exemple, n'est pas à l'abri d'un accident de la circulation ! Les promesses non tenues font partie de la vie quotidienne, et nous devons accepter cet état de fait, aussi bien dans nos vies personnelles qu'en matière de programmation asynchrone.

Dans la relation de promesse, l'initiateur doit donc disposer d'une méthode pour signaler qu'il ne peut pas tenir sa promesse. De son côté, le consommateur doit disposer d'une méthode pour vérifier l'affirmation de l'initiateur. Deuxièmement, en tant que consommateurs, nous sommes parfois impatients quant aux promesses qui nous sont faites ! Par conséquent, si l'initiateur peut suivre la progression de la réalisation de sa promesse, le consommateur doit également disposer d'un moyen de recevoir cette information. Troisièmement, le consommateur peut également annuler sa commande et signaler à l'initiateur qu'il n'a plus besoin des marchandises.

En ajoutant ces besoins à notre schéma, nous disposons désormais d'une vue d'ensemble de la relation :

promise_diagram_2

Examinons maintenant comment ces relations se manifestent dans le code.

La construction de la promesse et les chaînes de promesses

En matière de promesses, il existe en fait plusieurs propositions et spécifications. Celle utilisée dans Windows et WinJS porte le nom Common JS/Promises A. Elle formule une promesse, laquelle est renvoyée par un initiateur pour représenter une valeur à transmettre ultérieurement. Cette promesse prend la forme d'un objet associé à une fonction appelée then. Les consommateurs s'abonnement à la réalisation de la promesse en appelant la méthode then. (Dans Windows, les promesses prennent aussi en charge une fonction similaire appelée done, qui est utilisée dans les chaînes de promesses, comme nous le verrons très vite.)

Le consommateur transmet jusqu'à trois fonctions optionnelles à cette fonction, sous forme d'arguments, dans l'ordre suivant :

  1. Un gestionnaire Terminé. L'initiateur appelle cette fonction lorsque la valeur promise est disponible. Si cette valeur est déjà disponible (is ), le gestionnaire Terminé est appelé immédiatement (de façon synchrone) à partir de then.
  2. Un gestionnaire d'erreurs facultatif, appelé lorsqu'il s'avère impossible d'acquérir la valeur promise. Pour une promesse donnée, le gestionnaire Terminé n'est jamais appelé si le gestionnaire d'erreur l'a été.
  3. Un gestionnaire de progression facultatif, qui est appelé de façon périodique à partir de résultats intermédiaires, si l'opération prend en charge cette possibilité. (Dans WinRT, cela signifie que l'API a la valeur renvoyée IAsync[Action | Operation]WithProgress, celles dont la valeur est IAsync[Action | Operation] ne prenant pas en charge cette possibilité.)

Sachez que vous pouvez transmettre la valeur null pour l'ensemble de ces arguments, par exemple lorsque vous souhaitez attacher uniquement un gestionnaire d'erreurs et non pas un gestionnaire Terminé.

De l'autre côté de la relation, un consommateur peut abonner autant de gestionnaires qu'il le souhaite à la même promesse, en appelant then à plusieurs reprises. Il peut également partager la promesse avec d'autres consommateurs, qui peuvent aussi appeler then pour leur propre contenu. Cette possibilité est intégralement prise en charge.

Par conséquent, une promesse doit gérer des listes contenant l'intégralité des gestionnaires qu'elle reçoit, et les appeler au moment adéquat. Les promesses doivent également pouvoir être annulées, comme nous l'avons expliqué dans notre description complète de la relation.

Autre exigence de la spécification Promises A, la méthode then renvoie elle-même une promesse. Cette deuxième promesse est réalisée lorsque le gestionnaire Terminé envoyé au premier appel de la méthode promise.then renvoie une valeur, la valeur renvoyée étant transmise en tant que résultat de cette deuxième promesse. Examinez le fragment de code suivant :

 var promise1 = someOperationAsync();
var promise2 = promise1.then(function completedHandler1 (result1) { return 7103; } );
promise2.then(function completedHandler2 (result2) { });

Ici, la chaîne d'exécution consiste à démarrer someOperationAsync, qui renvoie promise1. Pendant que cette opération est en cours, nous appelons promise1.then, qui renvoie immédiatement promise2. Il va sans dire que la méthode completedHandler1 n'est pas appelée, sauf si le résultat de l'opération asynchrone est déjà disponible. Supposons que nous attendons encore le résultat. Dans ce cas, nous appelons directement promise2.then et une là encore, la méthode completedHandler2 n'est pas appelée pour le moment.

Supposons que par la suite, la méthode someOperationAsync renvoie la valeur 14618. La promesse promise1 est alors réalisée, et elle appelle donc completedHandler1 en utilisant cette valeur. result1 a donc la valeur 14618. completedHandler1 s'exécute alors et renvoie la valeur 7103. À ce stade, la promesse promise2 est réalisée, ce qui provoque l'appel de completedHandler2 avec 7103 comme valeur result2.

Que se passe-t-il si un gestionnaire Terminé renvoie une autre promesse ? La situation est alors gérée de façon quelque peu différente. Supposons que la méthode completedHandler1 du code ci-dessus renvoie une promesse comme celle-ci :

 var promise2 = promise1.then(function completedHandler1 (result1) {
    var promise2a = anotherOperationAsync();
    return promise2a;
});

Dans ce cas, la valeur result2 de completedHandler2 ne correspond pas directement à promise2a, mais à la valeur fulfillment de promise2a. En effet, le gestionnaire Terminé renvoie une promesse, promise2, telle qu'elle est renvoyée par promise1.then, qui elle-même est réalisée en utilisant le résultat de promise2a.

C'est précisément cette caractéristique qui rend possible la création de chaînes d'opération séquentielles asynchrones, dans lesquelles le résultat de chaque opération de la chaîne est transmis à l'opération suivante. Dépourvues de variables intermédiaires et de gestionnaires nommés, les chaînes de promesses ressemblent souvent à ce modèle :

 operation1().then(function (result1) {
    return operation2(result1)
}).then(function (result2) {
    return operation3(result2);
}).then(function (result3) {
    return operation4(result3);
}).then(function (result4) {
    return operation5(result4)
}).then(function (result5) {
    //And so on
});

Bien évidemment, chaque gestionnaire Terminé réalise en général d'autres opérations à partir du résultat reçu, mais cette structure de base reste commune à toutes les chaînes. Par ailleurs, toutes les méthodes then s'exécutent ici l'une après l'autre, car leur seule fonction consiste à enregistrer un gestionnaire Terminé spécifique et à renvoyer une autre promesse. Ainsi, à la fin du code, seule l'opération operation1 a démarré et aucun gestionnaire Terminé n'a été appelé. En revanche, plusieurs promesses intermédiaires issues des appels de la méthode then ont été créées et reliées les unes aux autres, afin de gérer la chaîne à mesure que les opérations séquentielles se déroulent.

Il faut ici remarquer que la même séquence peut être exécutée en imbriquant chacune des opérations suivantes dans le gestionnaire Terminé précédent, auquel cas vous ne disposez pas d'instruction return. Cependant, ces imbrications peuvent conduire à un véritable casse-tête en termes de mise en retrait, en particulier si vous commencez à ajouter des gestionnaires de progression et d'erreurs à chaque appel de la méthode then.

D'ailleurs, les promesses de WinJS offrent notamment la fonctionnalité suivante : les erreurs qui ont lieu dans une partie de la chaîne sont automatiquement propagées en bout de chaîne. Par conséquent, vous pouvez en principe attacher un seul gestionnaire d'erreurs au dernier appel de la méthode then, au lieu de mettre en place des gestionnaires à chaque niveau. Cependant, pour différentes raisons assez subtiles, ces erreurs sont omises si le dernier lien de la chaîne est un appel de la méthode then. Pour cette raison, WinJS fournit également une méthode done pour les promesses. Cette méthode accepte les mêmes arguments que then, mais elle indique que la chaîne est terminée (elle renvoie la valeur undefined plutôt qu'une autre promesse). Un gestionnaire d'erreurs attaché à la méthode done est ensuite appelé pour gérer les erreurs de l'ensemble de la chaîne. En outre, en l'absence de gestionnaire d'erreurs, done émet une exception au niveau de l'application, où elle peut être gérée par les événements window.onerror de WinJS.Application.onerror. Pour simplifier, toutes les chaînes doivent idéalement se terminer par done pour faire en sorte que les exceptions apparaissent en surface et soient gérées correctement.

Bien évidemment, si vous écrivez une fonction dont le but est de renvoyer la dernière promesse d'une longue chaîne d'appels de la méthode then, vous utiliserez quand même then à la fin : la responsabilité du traitement des erreurs appartient ensuite à l'appelant, qui peut utiliser cette promesse dans une autre chaîne.

Création de promesses : la classe WinJS.Promise

Si vous pouvez dans tous les cas créer vos propres classes de promesses autour de la spécification Promises A, cela représente beaucoup de travail et il est préférable de faire appel à une bibliothèque. Pour cette raison, WinJS fournit une classe de promesses flexible, performante et rigoureusement testée, appelée WinJS.Promise. Elle vous permet de créer facilement des promesses autour de différentes valeurs et opérations, sans devoir gérer les détails de la relation initiateur/consommateur, ni le comportement de la méthode then.

Lorsque vous en avez besoin, vous pouvez (et vous avez d'ailleurs tout intérêt à le faire) utiliser new WinJS.Promise ou une fonction d'assistance adaptée pour créer des promesses autour des opérations asynchrones et des valeurs existantes (synchrones). N'oubliez pas qu'une promesse n'est ni plus ni moins qu'une construction de code : elle ne doit pas obligatoirement inclure une opération asynchrone, ni quoi que ce soit dans un wrapper. De même, le simple fait d'inclure un morceau de code dans un wrapper au sein d'une promesse ne rend pas pour autant son exécution asynchrone . C'est en effet à vous de réaliser ce travail.

Pour prendre un exemple d'utilisation simple et direct de WinJS.Promise, supposons que nous souhaitons réaliser un calcul long (ajouter différents nombres de 1 à une valeur maximale) de façon asynchrone. Nous pourrions inventer notre propre mécanisme de rappel pour une telle routine, mais si nous l'incluons dans un wrapper, nous pouvons le joindre aux promesses d'autres API ou l'assembler sous forme de chaîne à ces promesses. (À ce propos, la fonction WinJS.xhr inclut la demande asynchrone XmlHttpRequest de JavaScript dans un wrapper au sein d'une promesse, et vous n'avez donc pas à vous préoccuper de la structure d'événements spécifique de cette dernière.)

Nous pouvons évidemment utiliser un processus de travail JavaScript pour réaliser le calcul long, mais pour les besoins de notre illustration, nous allons le laisser sur le thread d'interface utilisateur et utiliser setImmediate pour diviser l'opération en plusieurs étapes. Voici comment implémenter cette méthode dans une structure de promesse en utilisant WinJS.Promise :

 function calculateIntegerSum(max, step) {
    //The WinJS.Promise constructor's argument is an initializer function that receives 
    //dispatchers for completed, error, and progress cases.
    return new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) {
        var sum = 0;

        function iterate(args) {
            for (var i = args.start; i < args.end; i++) {
                sum += i;
            };

            if (i >= max) {
                //Complete--dispatch results to completed handlers
                completeDispatch(sum);
            } else {
                //Dispatch intermediate results to progress handlers
                progressDispatch(sum);
                setImmediate(iterate, { start: args.end, end: Math.min(args.end + step, max) });
            }
        }
            
        setImmediate(iterate, { start: 0, end: Math.min(step, max) });
    });
}

Lorsque la méthode new WinJS.Promise est appelée, le seul argument de son constructeur est une fonction initializer (qui est anonyme, dans notre cas). L'initialiseur encapsule l'opération à réaliser, mais sachez que la fonction elle-même s'exécute de façon synchrone sur le thread d'interface utilisateur. Par conséquent, si nous devions simplement réaliser un calcul long sans utiliser setImmediate, nous bloquerions intégralement le thread d'interface utilisateur à ce stade. Rappelons que le fait de placer du code dans une promesse ne lerend pas automatiquement asynchrone : la fonction de l'initialiseur doit configurer elle-même ce paramètre.

En termes d'arguments, la fonction d'initialiseur reçoit trois répartiteurs pour les différents cas pris en charge par les promesses (opération terminée, erreur et progression). Comme vous pouvez le constater, nous appelons ces répartiteurs à certains moments bien précis au cours de l'opération, en utilisant les arguments appropriés.

J'appelle ces fonctions des « répartiteurs », car ils ne jouent pas le même rôle que les gestionnaires que les consommateurs abonnent à la méthode then de la promesse (ou à la méthode done, comme vous le savez désormais). En arrière-plan, WinJS gère des tableaux de gestionnaires, ce qui permet à un nombre illimité de consommateurs de s'abonner à un nombre illimité de gestionnaires. Lorsque vous appelez l'un de ces répartiteurs, WinJS réalise une itération au sein de sa liste interne et appelle de votre part l'ensemble de ces gestionnaires. WinJS.Promise s'assure également que sa méthode then renvoie une autre promesse, car cela est requis pour former une chaîne.

Pour résumer, WinJS.Promise fournit l'ensemble des détails qui entourent une promesse. Vous pouvez ainsi vous concentrer sur l'opération centrale représentée par la promesse, qui prend la forme de la fonction d'initialiseur.

Fonctions d'assistance pour la création de promesses

La principale fonction d'assistance permettant de créer une promesse est la méthode statique WinJS.Promise.as, qui inclut dans un wrapper n'importe quelle valeur au sein d'une promesse. Un tel wrapper sur une valeur existante se retourne et appelle le gestionnaire Terminé transmis à la méthode then. Ceci vous permet plus précisément de traiter les valeurs arbitraires connues comme des promesses, que vous pouvez entremêler et mélanger (en les liant ou en les regroupant en chaîne) à d'autres promesses. En utilisant as sur une promesse existante, cette promesse est renvoyée.

L'autre fonction d'assistance statique est WinJS.Promise.timeout, qui fournit un wrapper très pratique autour de setTimeout et setImmediate. Vous pouvez également créer une promesse qui annule une seconde promesse si cette dernière n'est pas réalisée au bout d'un certain nombre de millisecondes.

Sachez que les promesses timeout qui entourent setTimeout et setImmediate sont elles-mêmes réalisées avec la valeur undefined. Une question revient souvent : « Comment ces promesses peuvent-elles être utilisées pour fournir un autre résultat une fois le délai expiré ? ». Pour trouver la réponse, rappelons que then renvoie une autre promesse qui est réalisée avec la valeur renvoyée par le gestionnaire Terminé. Prenons par exemple cette ligne de code :

 var p = WinJS.Promise.timeout(1000).then(function () { return 12345; });

Elle crée la promesse p, qui sera réalisée avec la valeur 12345 au bout d'une seconde. En d'autres termes, WinJS.Promise.timeout(…).then(function () { return <valeur>} ) est un schéma qui permet de renvoyer <valeur> au bout du délai d'expiration donné. Si la valeur <valeur> est elle-même une autre promesse, l'opération revient alors à fournir la valeur de réalisation de cette promesse au bout d'un certain temps après l'expiration du délai.

Annulation et génération d'erreurs de promesse

Dans le code que nous venons d'examiner, vous avez peut-être remarqué trois problèmes. En premier lieu, il est impossible d'annuler l'opération une fois qu'elle a démarré. Deuxièmement, le traitement des erreurs n'est pas vraiment optimal.

Dans les deux cas, le problème est que les fonctions qui créent une promesse, par exemple calculateIntegerSum, doivent toujours renvoyer une promesse. Si une opération ne se termine pas ou n'est jamais démarrée, la promesse se retrouve à l'état d'erreur. Par conséquent, la promesse n'a pas de résultat à transmettre aux gestionnaires Terminé et n'en aura d'ailleurs jamais : elle appelle uniquement ses gestionnaires d'erreurs. En effet, si un consommateur appelle la méthode then pour une promesse qui se trouve déjà en état d'erreur, cette promesse appelle immédiatement (de façon synchrone) le gestionnaire d'erreurs indiqué à la méthode then.

Une promesse WinJS.Promise peut entrer dans un état d'erreur pour deux raisons : si le consommateur appelle sa méthode cancel ou si le code de la fonction d'initialiseur appelle le répartiteur d'erreurs. Lorsque cela se produit, les gestionnaires d'erreurs reçoivent la valeur d'erreur interceptée ou propagée dans la promesse. Si vous créez une opération dans une promesse WinJS.Promise, vous pouvez également utiliser une instance de WinJS.ErrorFromName. Il s'agit simplement d'un objet JavaScript contenant une propriété name qui identifie l'erreur et une propriété message offrant plus de détails. Par exemple, lorsqu'une promesse est annulée, les gestionnaires d'erreurs reçoivent un objet d'erreur dont les propriétés name et message portent la valeur « Canceled ».

Mais que faire si vous ne pouvez même pas démarrer l'opération ? Par exemple, si vous appelez la méthode calculateIntegerSum en utilisant des arguments incorrects (0, 0, par exemple), en principe elle n'essaie même pas de lancer le décompte et renvoie une promesse dans un état d'erreur. C'est le but de la méthode statique WinJS.Promise.wrapError. Elle accepte comme paramètre une instance de WinJS.ErrorFromName et renvoie une promesse dans un état d'erreur, qui dans ce cas est renvoyée en lieu et place d'une nouvelle instance WinJS.Promise.

Autre partie importante, bien qu'un appel de la méthode cancel de la promesse met la promesse elle-même dans un état d'erreur, comment faire pour arrêter l'opération asynchrone en cours ? Dans l'implémentation précédente de calculateIntegerSum, la méthode setImmediate est appelée de façon répétée jusqu'à ce que l'opération soit terminée, quel que soit l'état de la promesse que nous avons créée. En effet, si l'opération appelle le répartiteur Terminé après l'annulation de la promesse, cette dernière ignore que l'opération est terminée.

Ensuite, nous devons faire en sorte que la promesse signale à l'opération qu'elle n'a plus besoin de poursuivre son travail. Pour cette raison, le constructeur WinJS.Promise accepte un deuxième argument de fonction, qui est appelé en cas d'annulation de la promesse. Dans notre exemple, l'appel de cette fonction doit pouvoir empêcher l'appel suivant de la méthode setImmediate, pour arrêter ainsi le calcul. Voici à quoi ressemble le code une fois que la gestion des erreurs a été améliorée :

 function calculateIntegerSum(max, step) {
    //Return a promise in the error state for bad arguments
    if (max < 1 || step < 1) {
        var err = new WinJS.ErrorFromName("calculateIntegerSum", "max and step must be 1 or greater");
        return WinJS.Promise.wrapError(err);
    }

    var _cancel = false;

    //The WinJS.Promise constructor's argument is an initializer function that receives 
    //dispatchers for completed, error, and progress cases.
    return new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) {
        var sum = 0;

        function iterate(args) {
            for (var i = args.start; i < args.end; i++) {
                sum += i;
            };

            //If for some reason there was an error, create the error with WinJS.ErrorFromName
            //and pass to errorDispatch
            if (false /* replace with any necessary error check -- we don’t have any here */) {
                errorDispatch(new WinJS.ErrorFromName("calculateIntegerSum (scenario 7)", "error occurred"));
            }

            if (i >= max) {
                //Complete--dispatch results to completed handlers
                completeDispatch(sum); 
            } else {
                //Dispatch intermediate results to progress handlers
                progressDispatch(sum);

                //Interrupt the operation if canceled
                if (!_cancel) {
                    setImmediate(iterate, { start: args.end, end: Math.min(args.end + step, max) });
                }
            }
        }
            
        setImmediate(iterate, { start: 0, end: Math.min(step, max) });
    },
    //Cancellation function for the WinJS.Promise constructor
    function () {
        _cancel = true;
    });
}

Au final, le fait de créer plusieurs instances de WinJS.Promise permet plusieurs usages. Par exemple, si vous disposez d'une bibliothèque qui communique avec un service Web par le biais d'une autre méthode asynchrone, vous pouvez inclure ces opérations dans un wrapper au sein des promesses. Vous pouvez également utiliser une nouvelle promesse pour combiner dans une seule promesse plusieurs opérations asynchrones (ou d'autres promesses) issues de différentes sources, afin de contrôler toutes les relations concernées. Dans le code d'un initialiseur de WinJS.Promise, vous pouvez évidemment intégrer vos propres gestionnaires pour d'autres opérations asynchrones et leurs promesses. Ces gestionnaires peuvent être utilisés pour encapsuler des mécanismes de relance automatique en cas d'expiration des délais réseau, par exemple, pour se raccorder à une interface générique de progression de mise à jour ou encore pour ajouter des fonctionnalités d'arrière-plan de journalisation ou d'analyse d'audience. Dans tous ces cas, le reste de votre code n'a jamais besoin de connaître les détails et peut simplement traiter les promesses du côté consommateur.

Dans le même esprit, il est relativement simple d'inclure un processus de travail JavaScript dans un wrapper au sein d'une promesse, pour qu'il ressemble à d'autres opérations asynchrones dans WinRT et qu'il se comporte de la même manière. Comme vous le savez peut-être, les processus de travail envoient leurs résultats par le biais d'un appel postMessage qui émet un événement message sur l'objet du processus de travail de l'application. Le code suivant relie ensuite cet événement à une promesse réalisée avec les résultats qui sont fournis dans le message :

 // This is the function variable we're wiring up.
var workerCompleteDispatch = null;

var promiseJS = new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) {
    workerCompleteDispatch = completeDispatch;
});

// Worker is created here and stored in the 'worker' variable

// Listen for worker events
worker.onmessage = function (e) {
    if (workerCompleteDispatch != null) {
        workerCompleteDispatch(e.data.results); /* event args depends on the worker */
    }
}

promiseJS.done(function (result) {
    // Output for JS worker
});

Pour élargir ce code en vue de gérer les erreurs issues du processus de travail, vous devez enregistrer le répartiteur d'erreurs dans une autre variable, faire en sorte que le gestionnaire d'événements message vérifie les informations d'erreur dans ses arguments d'événement, puis appeler le répartiteur d'erreurs en lieu et place du répartiteur Terminé.

Joindre des promesses parallèles

Comme les promesses sont souvent utilisées pour inclure des opérations asynchrones dans un wrapper, il est tout à fait possible que plusieurs opérations soient en cours au même moment. Dans ce cas, il peut être utile de savoir à quel moment l'une des promesses d'un groupe est réalisée ou à quel moment toutes les promesses du groupe sont réalisées. Les fonctions statiques WinJS.Promise.any et WinJS.Promise.join sont justement là pour ça.

Les deux fonctions acceptent un tableau de valeurs ou un objet comportant des propriétés de valeur. Ces valeurs peuvent être des promesses, et les éventuelles valeurs qui ne sont pas des promesses sont incluses dans un wrapper avec WinJS.Promise.as, de telle sorte que l'intégralité du tableau ou de l'objet se compose de promesses.

Voici les caractéristiques de la méthode any :

  • any crée une seule promesse qui est réalisée lorsque l'une de ses promesses est réalisée ou échoue en raison d'une erreur (OU logique). En substance, anyrattache les gestionnaires Terminé à toutes ces promesses et dès qu'un gestionnaire Terminé est appelé, il appelle les gestionnaires Terminé que la promesse anya elle-même reçu.
  • Une fois que la promesse anyest réalisée (c'est-à-dire lorsque la première promesse de la liste est réalisée), les autres opérations de la liste sont exécutées, en appelant les gestionnaires Terminé, les gestionnaires d'erreur ou les gestionnaires de progression affectés individuellement aux différentes promesses.
  • Si vous annulez la promesse à partir de any, toutes les promesses de la liste sont annulées.

En ce qui concerne join :

  • join crée une seule promesse qui est réalisée lorsque toutes ses promesses sont réalisées ou échouent en raison d'une erreur (ET logique). En substance, joinrattache les gestionnaires Terminé et les gestionnaires d'erreurs à toutes ces promesses, et attend que tous ces gestionnaires aient été appelés, avant d'appeler les gestionnaires Terminé qu'elle a elle-même reçu.
  • La promesse join signale également sa progression aux éventuels gestionnaires de progression que vous fournissez. Dans ce cas, le résultat intermédiaire est un tableau de résultats issus des promesses individuelles réalisées jusque là.
  • Si vous annulez la promesse à partir de join, toutes les promesses en attente sont annulées.

En dehors de any et join, il existe deux autres méthodes statiques WinJS.Promise à connaître, car elles peuvent s'avérer pratiques :

  • is détermine si une valeur arbitraire est une promesse et renvoie une valeur booléenne. Pour simplifier, elle vérifie qu'il s'agit d'un objet comportant une fonction appelée « then » et ne teste pas « done ».
  • theneachapplique des gestionnaires Terminé, des gestionnaires d'erreurs et des gestionnaires de progression à un groupe de promesses (en utilisant then) et renvoie les résultats sous forme d'un groupe de valeurs au sein d'une promesse. La valeur des gestionnaires peut être null.

Promesses parallèles avec résultats séquentiels

Avec WinJS.Promise.join et WinJS.Promise.any, nous pouvons travailler avec des promesses parallèles, c'est-à-dire avec des opérations parallèles asynchrones. La promesse renvoyée par join est là encore réalisée lorsque toutes les promesses d'un tableau sont réalisées. Cependant, il est fort probable que ces promesses se réalisent dans un ordre aléatoire. Comment faire si vous disposez d'un ensemble d'opérations qui peuvent s'exécuter de cette manière, mais que vous souhaitez traiter leurs résultats dans un ordre bien défini, c'est-à-dire dans l'ordre dans lequel leurs promesses apparaissent dans le tableau ?

Pour parvenir au but escompté, vous devez joindre chaque promesse suivante à la méthode join de toutes les méthodes précédentes. Le petit extrait de code qui figure au début de ce billet réalise précisément cette opération. Voici de nouveau le code, légèrement réécrit pour rendre les promesses plus explicites. (Supposez que listest un tableau de valeurs utilisées comme arguments d'un appel asynchrone doOperationAsync susceptible de générer une promesse) :

 list.reduce(function callback (prev, item, i) {
    var opPromise = doOperationAsync(item);
    var join = WinJS.Promise.join({ prev: prev, result: opPromise});

    return join.then(function completed (v) {
        console.log(i + ", item: " + item+ ", " + v.result);
    });
})

Pour comprendre ce code, nous devons tout d'abord comprendre comment fonctionne la méthode reduce du tableau. Pour chaque élément du tableau, reduce appelle l'argument de fonction ici appelé callback, qui reçoit quatre arguments (seuls trois d'entre eux sont utilisés dans le code) :

  • prev Valeur renvoyée par l'appel précédent de callback (valeur null pour le premier élément).
  • item Valeur actuelle issue du tableau.
  • i Index de l'élément dans la liste.
  • source Tableau d'origine.

Pour le premier élément de la liste, nous obtenons une promesse que j'appellerai opPromise1. Comme la valeur de prev est null, nous joignons [WinJS.Promise.as(null), opPromise1] . Remarquez cependant que nous ne renvoyons pas la méthode join elle-même. En lieu et place, nous attachons un gestionnaire Terminé (que j'ai appelé completed) à cette méthode join et renvoyons la promesse de sa méthode then.

N'oubliez pas que la promesse renvoyée par then sera réalisée lorsque le gestionnaire Terminé renverra une valeur. Par conséquent, nous renvoyons à partir de callback une promesse qui n'est pas réalisée tant que le gestionnaire completed du premier élément n'a pas traité les résultats de opPromise1. Si vous regardez le résultat d'une méthode join, elle est réalisée au moyen d'un objet contenant les résultats issus des promesses de la liste d'origine. Par conséquent, la valeur de réalisation v contiendra à la fois une propriété prev et une propriété result, la valeur de cette dernière correspondant au résultat de opPromise1.

Avec l'élément suivant de list, callback reçoit une propriété prev qui contient la promesse issue de la méthode join.then précédente. Nous créons ensuite une nouvelle jointure de opPromise1.then et opPromise2. Par conséquent, cette jointure ne se termine pas tant que opPromise2 n'a pas été réalisée et que le gestionnaire Terminé de opPromise1 n'a pas renvoyé de valeur. Et voilà ! Le gestionnaire completed2 que nous attachons à cette méthode join n'est pas appelé tant que completed1 n'a pas renvoyé de valeur.

Les mêmes dépendances sont progressivement construites pour chaque élément de la liste. La promesse issue de join.then pour l'élément n n'est pas réalisée tant que completedn** ne renvoie pas de valeur. Ainsi, les gestionnaires Terminé sont forcément appelés dans le même ordre que list.

Pour conclure

Dans ce billet, nous avons constaté qu'une promesse n'est en substance qu'une construction de code ou une convention d'appel (quoique très puissante), qui permet de représenter une relation spécifique entre un initiateur, qui doit fournir des valeurs à une heure ultérieure arbitraire, et un consommateur, qui souhaite savoir quand ces valeurs sont disponibles. Ainsi, les promesses sont idéales pour représenter des résultats issus d'opérations asynchrones. Elles sont très souvent utilisées dans les applications du Windows Store écrites en JavaScript. La spécification des promesses permet également la création de chaînes d'opérations séquentielles asynchrones, chaque résultat intermédiaire passant d'un lien à un autre.

WinJS, la Bibliothèque Windows pour JavaScript, fournit une implémentation fiable des promesses, que vous pouvez utiliser pour inclure dans un wrapper n'importe quel type d'opération de votre choix. Elle fournit également des fonctions d'assistance pour certains scénarios courants, par exemple pour regrouper des promesses dans le cadre d'opérations parallèles. Grâce aux avantages de WinJS, vous pouvez travailler de façon très efficace avec des opérations asynchrones.

Kraig Brockschmidt

Chef de projet, équipe Écosystème Windows

Auteur de Programming Windows 8 Apps in HTML, CSS, and JavaScript