IdentifiantMot de passe
Loading...
Mot de passe oubli� ?Je m'inscris ! (gratuit)
Voir le flux RSS

Le Blog d'un Ninja codeur

[Actualit�] Comprendre les Promises en JavaScript / TypeScript

Note : 6 votes pour une moyenne de 2,00.
par , 26/06/2017 � 09h00 (8118 Affichages)
Nom : JS TS.png
Affichages : 72614
Taille : 7,3 Ko

Cet article a pour but de pr�senter un peu plus pos�ment et en d�tail les Promises (ou Promesses en fran�ais), un concept qui avait �t� d�j� abord� dans un article que j'avais traduit en 2015 : Le futur de l'asynchrone en JavaScript.

Je pense que c'est d'autant plus int�ressant que la norme ES2015 (ou ES6) s'est depuis largement diffus�e c�t� navigateurs, serveurs, transpileurs et d�veloppeurs au point que les Promises sont devenues un �l�ment central de la programmation asynchrone sur lequel d'autres fonctionnalit�s de la norme ECMAScript se construisent, comme async/await.

Bien que les exemples dans cet article soient r�dig�s en TypeScript, les diff�rences avec JavaScript ne concernent que les annotations de typage qui peuvent �tre vues ici comme de la documentation suppl�mentaire. Cela ne devrait pas g�ner la compr�hension de quelqu'un habitu� � JavaScript.


1. Traitements asynchrones et callbacks

JavaScript est un langage fonctionnel reposant sur une architecture asynchrone (cf. Les bases de l'asynchrone en JavaScript). Il n'est donc pas surprenant qu'historiquement, l'appel � des traitements asynchrones se faisait � l'aide de callbacks, ces fonctions pass�es en argument, permettant de poursuivre l'ex�cution du programme suite � la r�alisation du traitement asynchrone.

Il existe de nombreuses fonctions asynchrones dans les API JavaScript, la plus connue �tant sans doute XMLHttpRequest.send(), mais pour des questions pratiques et p�dagogiques, nous allons nous construire notre propre fonction asynchrone qui simulera la r�cup�ration d'une donn�e sous la forme d'un nombre.

Cette fonction asynchrone faite maison est nomm�e de fa�on tr�s imaginative getData(). Elle produit un nombre al�atoire qui sera ensuite transmis � la fonction callback pr�alablement pass�e en argument. Et pour simuler les erreurs �ventuelles pouvant survenir durant un traitement asynchrone, cette fonction getData() � plante � al�atoirement dans 25 % des cas.

Code typescript : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
type Callback = (error?: Error, data?: number) => void;
 
function getData(callback: Callback) {
    setTimeout(function setTimeoutCB(counter: number) {
        if (Math.random() < 0.25) {
            callback(new Error("Error in retrieving data."));
        }
        else {
            let data = Math.random();
            callback(undefined, data);
        }
    }, 500);
} // getData
D�finition de la fonction asynchrone getData()

On notera que cette fonction asynchrone getData() utilise une convention largement r�pandue en JavaScript qui est souvent nomm�e le callback pattern reposant sur les quatre principes suivants :
  • une seule fonction callback g�rant � la fois le succ�s et l'�chec ;
  • la fonction callback est appel�e une et une seule fois. Soit en cas de succ�s ou d'�chec ;
  • la fonction callback est le dernier argument de la fonction asynchrone ;
  • le premier param�tre de la fonction callback repr�sente l'erreur, le second repr�sente le r�sultat en cas de succ�s : function (error, result) { ... }


Dans le cadre de cet exemple, la r�cup�ration des donn�es se fait via un appel � cette fonction. Nous supposerons ici trois traitements successifs, process1(), process2() et process3(), chacun r�cup�rant une donn�e � l'aide de getData(). Voici � quoi pourrait ressembler la fonction du premier traitement :

Code typescript : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
function process1() {
    getData((error, data) => {
        if (error)
            console.log(error);
        else {
            console.log("Process 1:", data);
            process2();
        }
    });
} // process1
D�finition du premier traitement

Il s'agit �videmment d'un exemple minimaliste, car on remarque que cette fonction process1() ne fait pas grand-chose � part g�rer un �ventuel cas d'erreur, afficher la donn�e r�cup�r�e via getData() et appeler le traitement suivant, process2().

Les fonctions pour les deux autres traitements sont tr�s similaires :

Code typescript : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function process2() {
    getData((error, data) => {
        if (error)
            console.log(error);
        else {
            console.log("Process 2:", data);
            process3();
        }
    });
} // process2
 
function process3() {
    getData((error, data) => {
        if (error)
            console.log(error);
        else {
            console.log("Process 3:", data);
        }
    });
} // process3
D�finition des deux autres traitements

Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
Process 1: 0.062177133421918995
Process 2: 0.2886812664927907
Process 3: 0.2154450024357153
Exemple de sortie sans erreur

Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
Process 1: 0.41092615721662096
Error: Error in retrieving data.
    at Timeout.setTimeoutCB [as _onTimeout] (callback.js:13:22)
    at ontimeout (timers.js:386:14)
    at tryOnTimeout (timers.js:250:5)
    at Timer.listOnTimeout (timers.js:214:5)
Exemple de sortie avec erreur

La fonction process2() appelle getData() puis encha�ne sur la fonction process3() qui appelle � son tour getData() pour enfin conclure l'ex�cution du programme.

Cette fa�on de programmer des appels � des fonctions asynchrones est tr�s classique. Il se trouve que dans cet exemple, simplifi� � l'extr�me, cela ne pose pas de probl�me particulier, mais il suffit que le code soit un peu plus gros, avec des fonctions d'appel dans le d�sordre et diss�min�es dans plusieurs fichiers source pour aboutir � ce qui est commun�ment appel� la programmation spaghetti et qui est avec la � pyramide de la mort � (succession excessive d'indentations), le principal sympt�me de � l'enfer des callbacks �.

�a peut vite partir dans tous les sens et peut donner la migraine lorsqu'il s'agit de d�boguer ou de retoucher le code.

C'est l� qu'entre en jeu un nouveau concept, les Promises.


2. La promesse des Promises

Il y a mille et une mani�res d'expliquer ce que sont les Promises, concept un peu myst�rieux au premier abord. Plut�t que d'employer des m�taphores, parfois d�routantes et trompeuses, je pense que le plus simple est de pr�senter les choses sous un angle concret et de ne pas trop se focaliser sur le terme en lui-m�me.

Une Promise est la transformation d'une fonction asynchrone en un objet (au sens JavaScript du terme) afin de faciliter la manipulation de ladite fonction asynchrone. Dans le domaine de la programmation, cette transformation d'une fonction en un objet est parfois appel�e r�ification. C'est d'ailleurs un design pattern puissant.

Dans notre exemple, il s'agit donc de transformer notre fonction asynchrone getData() en un objet de type Promise. Pour se faire, la norme ES2015 propose un constructeur de la forme suivante :
Code typescript : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
new Promise((resolve, reject) => {
 /* appel à la fonction asynchrone */
});

On notera que la forme ci-dessus n'est pas typ�e pour des raisons de clart�, une version typ�e est indiqu�e un peu plus loin.

Le constructeur d'une Promise prend en param�tre une fonction, d�sign�e par convention comme � l'ex�cuteur �. Cet ex�cuteur prend deux fonctions en argument : resolve et reject. La fonction resolve() sera appel�e en cas de succ�s de la fonction asynchrone, tandis que la fonction reject() sera appel�e en cas d'�chec.

Dans le cadre de notre exemple avec getData(), cela pourrait ressembler � ceci :
Code typescript : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
type PromiseResolve<T> = (value?: T | PromiseLike<T>) => void;
type PromiseReject = (error?: any) => void;
 
function getDataPromise() {
    return new Promise((resolve: PromiseResolve<number>, reject: PromiseReject): void => {
        getData((error, data) => {
            if (error) reject(error)
            else resolve(data)
        });
    });
} // getDataPromise
R�ification de la fonction asynchrone

Ci-dessus, la fonction getDataPromise() g�n�re une Promise de getData(). On peut constater que la fonction callback pass�e en argument de getData() appelle reject() en cas d'erreur, et resolve() si tout se passe correctement.

Pour appeler notre fonction asynchrone getData() � partir de cet objet g�n�r� par le constructeur new Promise(), nous pouvons utiliser une m�thode nomm�e then() de la fa�on suivante :

Code typescript : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
getDataPromise().then(
    data => { // resolve()
        console.log("Process 1:", data);
        return getDataPromise();
    }
)
Appel de la fonction asynchrone via la m�thode then()

La m�thode then() va appeler l'ex�cuteur de la Promise qui lui-m�me appellera la fonction asynchrone. Cette m�thode then() prend en th�orie deux param�tres, le second �tant optionnel. Ces param�tres correspondent respectivement � la fonction resolve() et reject() qui seront transmis � l'ex�cuteur. Dans l'exemple ci-dessus, seul le param�tre correspondant � la fonction resolve() a �t� sp�cifi�.

� noter qu'il n'est pas obligatoire de sp�cifier la fonction reject() dans l'appel � la m�thode then(), c'est m�me d�conseill�. Si une erreur survient et que la fonction reject() n'est pas sp�cifi�e, alors une exception sera signal�e qui pourra �tre r�cup�r�e ult�rieurement via une autre m�thode catch().

Avant de poursuivre, voici un petit r�capitulatif des notions abord�es autour des Promises :
  • Fonction asynchrone : fonction dont on souhaite obtenir un r�sultat ;
  • Promise : r�ification de la fonction asynchrone ;
  • Ex�cuteur de la Promise : fonction appel�e, entre autres, via la m�thode then() de la Promise et charg�e d'appeler la fonction asynchrone ;
  • resolve : fonction appel�e en cas de succ�s de la fonction asynchrone ;
  • reject : fonction appel�e en cas d'�chec de la fonction asynchrone ;
  • then : m�thode de la Promise pour appeler l'ex�cuteur.


Comme on l'a vu pr�c�demment, transformer une fonction asynchrone en Promise (on lit parfois � promissification �) consiste en deux �tapes :
  • r�ifier la fonction asynchrone via le constructeur de Promise en prenant soin d'�crire l'ex�cuteur de la fonction asynchrone de telle sorte qu'il appelle la fonction resolve() en cas de succ�s, et reject() en cas d'�chec ;
  • l'appel de la fonction asynchrone peut se faire via la m�thode then() de la Promise nouvellement cr��e. C'est dans l'appel � cette m�thode then() que se d�finissent concr�tement les fonctions auxiliaires resolve() et reject(), et qui seront appel�es par l'ex�cuteur de la fonction asynchrone.



3. Composition de Promises

L'�tat d'une Promise peut �voluer au cours de son existence.

Lors de sa cr�ation, une Promise est dans un �tat � en attente � (pending), sous-entendu, en attente d'�tre r�solue.
Lorsque la m�thode then() de cette Promise est appel�e, son �tat devient soit honor� (fullfilled) en cas de succ�s du traitement asynchrone sous-jacent, ou rompu (rejected) en cas d'�chec. Une Promise honor�e ou rompue est une Promise acquitt�e (settled) ou encore r�solue, bien que ce dernier terme puisse porter � confusion avec la fonction resolve() qui correspond � une Promesse honor�e uniquement.

Pour une Promise prise isol�ment, ces changements d'�tat n'ont pas une grande importance. Ce n'est plus le cas lorsqu'il s'agit de cha�ner, de composer plusieurs Promises entre elles.

La m�thode then() renvoyant elle-m�me une Promise, il est possible d'encha�ner les appels � cette m�thode.

Dans le cadre de notre exemple, on aurait pu �crire le code suivant :
Code typescript : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
getDataPromise()
    .then(
        data => { // resolve()
            console.log("Process 1:", data);
            return getDataPromise();
        },
        error => { // reject()
            console.log(error);
        })
    .then(
        data => { // resolve()
            console.log("Process 2:", data);
            return getDataPromise();
        },
        error => { // reject()
            console.log(error);
        })
    .then(
        data => { // resolve()
            console.log("Process 3:", data);
        },
        error => { // reject()
            console.log(error);
        })
;
Composition d'appels de la m�thode then() � deux arguments (non recommand�)

On notera tout d'abord la ressemblance, voulue, avec les fonctions process1(), process2() et process3() du chapitre 2 sur les fonctions callback, sauf qu'ici il n'a pas �t� n�cessaire de cr�er ces fonctions interm�diaires, limitant ainsi le recours au code spaghetti, tout en �vitant la � pyramide de la mort �.

Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
Process 1: 0.9523288000061796
Process 2: 0.7071524761970143
Process 3: 0.31277126054349025
Exemple de sortie sans erreur avec la m�thode then() � deux arguments

Ensuite, on remarquera le renvoi d'une nouvelle Promise (return getDataPromise()) dans les deux premiers appels � then(), dans la partie d�di�e � la fonction resolve(). C'est le fait de retourner une nouvelle Promise qui permet � l'appel � la m�thode then() suivante de produire un nouveau r�sultat, ici un nombre al�atoire. Sans cela, le second et le troisi�me appel � then() op�reraient sur une Promise d�j� acquitt�e, issue du premier appel � then(), avec un r�sultat non d�fini (undefined).

C'est d'ailleurs ce qui se passe dans le code ci-dessus en cas d'erreur. La partie correspondant � la fonction reject() ne renvoie pas de nouvelle Promise.

Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
Process 1: 0.6955174514497151
Error: Error in retrieving data.
    at Timeout.setTimeoutCB [as _onTimeout] (promise.js:13:22)
    at ontimeout (timers.js:386:14)
    at tryOnTimeout (timers.js:250:5)
    at Timer.listOnTimeout (timers.js:214:5)
Process 3: undefined
Exemple de sortie avec erreur avec la m�thode then() � deux arguments

On notera dans l'exemple de sortie ci-dessus la valeur undefined au niveau du Process 3.

Le code produit plus haut ne se comporte donc pas tout � fait comme son homologue avec les fonctions callback classiques. Ici, une erreur n'emp�che pas la poursuite des appels.

Il est assez rare que cela soit un comportement souhait�, et comme vu dans le chapitre pr�c�dent, il est d�conseill� de sp�cifier la fonction reject() lors de l'appel � la m�thode then(). Il faut pr�f�rer l'utilisation d'une autre m�thode, catch(), qui n'est qu'en fait une version simplifi�e de then(), ne prenant qu'un seul argument, la fonction reject().

Code typescript : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
getDataPromise()
    .then(
        data => { // resolve()
            console.log("Process 1:", data);
            return getDataPromise();
        })
    .then(
        data => { // resolve()
            console.log("Process 2:", data);
            return getDataPromise();
        })
    .then(
        data => { // resolve()
            console.log("Process 3:", data);
        })
    .catch(error => { // reject()
        console.log(error);
    })
;
Composition d'appels de la m�thode then() � un seul argument, avec la m�thode catch() (recommand�)

Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
Process 1: 0.29398199861957175
Process 2: 0.9661893214860926
Process 3: 0.9223972522278627
Exemple de sortie sans erreur avec la m�thode catch()

Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
Process 1: 0.20758402318588276
Error: Error in retrieving data.
    at Timeout.setTimeoutCB [as _onTimeout] (promise.js:13:22)
    at ontimeout (timers.js:386:14)
    at tryOnTimeout (timers.js:250:5)
    at Timer.listOnTimeout (timers.js:214:5)
Exemple de sortie avec erreur avec la m�thode catch()

Lorsqu'une erreur se produit, celle-ci se propage de fa�on descendante jusqu'� la premi�re m�thode catch() trouv�e. La m�thode catch() peut appara�tre tout au long de la cha�ne, � condition que les catch() interm�diaires prennent soin de propager l'erreur tout au long de la cha�ne � l'aide de l'instruction throw. Cela peut �tre utile pour g�rer les erreurs d�s qu'elles surviennent, dans une logique de travail en �quipe o� chaque d�veloppeur est responsable de la gestion des erreurs de son domaine fonctionnel, et aussi pour �viter d'avoir un gros switch dans le catch() final. Il y aurait beaucoup � dire sur la mani�re de g�rer les erreurs avec les Promises, mais cela d�passerait probablement le cadre de cette introduction.

Il est possible de combiner les Promises autrement qu'avec then(). Pr�sentons deux autres m�thodes standards offertes dans la norme ES2015 : all() et race().

La m�thode all() prend en param�tres une s�quence de Promises et renvoie une Promise qui est honor�e si et seulement si toutes les Promises pass�es en param�tres sont honor�es �galement. La valeur finale �tant un tableau des valeurs des Promises honor�es. Dans le cas contraire, si au moins l'une des Promises pass�es en param�tres a �t� rompue, alors la Promise renvoy�e par la m�thode all() sera rompue �galement. En gros, c'est tout ou rien.

Code typescript : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
Promise.all([getDataPromise(), getDataPromise(), getDataPromise()])
    .then(
        data => {
            console.log("Success!", data);
        })
    .catch(error => {
        console.log(error);
    })
;
Exemple d'utilisation de la m�thode all()

Code : S�lectionner tout - Visualiser dans une fen�tre � part
Success! [ 0.5930896059730313, 0.4881301731930028, 0.338379519116232 ]
Exemple de sortie sans erreur de la m�thode all()

La m�thode race() quant � elle prend �galement en param�tres une s�quence de Promises et renvoie une Promise qui est honor�e si au moins une des Promises pass�es en param�tres a �t� honor�e. La valeur prise en compte �tant celle de la premi�re Promise � avoir �t� honor�e, d'o� le nom � race � (course en fran�ais). Ce n'est que si toutes les Promises pass�es en param�tres ont �t� rompues que la Promise renvoy�e par race() sera rompue.

Code typescript : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
Promise.race([getDataPromise(), getDataPromise(), getDataPromise()])
    .then(
        data => {
            console.log("Success!", data);
        })
    .catch(error => {
        console.log(error);
    })
;
Exemple d'utilisation de la m�thode race()

Code : S�lectionner tout - Visualiser dans une fen�tre � part
Success! 0.2673875038150235
Exemple de sortie sans erreur de la m�thode race()


4. Conclusion

Terminons cet article sur quelques consid�rations de compatibilit�. Si votre plateforme cible ne supporte pas les Promises (et plus g�n�ralement la norme ES2015), il est possible d'avoir recours � un polyfill. Il en existe de nombreux sur la toile, 100 % compatibles avec la norme ES2015. Il y a aussi le transpilateur Babel qui inclut un polyfill pour les Promises.

Pour compiler du code TypeScript en ES5 tout en utilisant les Promises, il convient d'utiliser un polyfill tiers comme indiqu� pr�c�demment, et d'inclure la biblioth�que es2015.promise dans l'option lib du fichier de projet tsconfig.json.

Comme on vient de le voir, les Promises sont une avanc�e tr�s appr�ciable pour mieux structurer son code asynchrone, et ceci au prix d'une abstraction somme toute minime. La conversion � partir du callback pattern historique est relativement directe. Cependant, m�me si le code r�sultant est davantage lin�aire, il reste encore assez �loign� � du code synchrone, imp�ratif, en g�n�ral plus facile � appr�hender. Mais �a, c'est le r�le des instructions async/await initialement pr�vues pour la version ES2016, mais report�es pour cette ann�e 2017, et qui pourront faire l'objet d'un article d�di�.

Envoyer le billet � Comprendre les Promises en JavaScript / TypeScript � dans le blog Viadeo Envoyer le billet � Comprendre les Promises en JavaScript / TypeScript � dans le blog Twitter Envoyer le billet � Comprendre les Promises en JavaScript / TypeScript � dans le blog Google Envoyer le billet � Comprendre les Promises en JavaScript / TypeScript � dans le blog Facebook Envoyer le billet � Comprendre les Promises en JavaScript / TypeScript � dans le blog Digg Envoyer le billet � Comprendre les Promises en JavaScript / TypeScript � dans le blog Delicious Envoyer le billet � Comprendre les Promises en JavaScript / TypeScript � dans le blog MySpace Envoyer le billet � Comprendre les Promises en JavaScript / TypeScript � dans le blog Yahoo

Mis � jour 28/06/2017 � 09h15 par yahiko

Cat�gories
TypeScript , Javascript , D�veloppement Web

Commentaires

  1. Avatar de blbird
    • |
    • permalink
    Merci pour cet article.

    Pour avoir utilis� les Promises intens�ment avec un serveur Nodes.Js, entre autres, une chose est tr�s importante, par exemple si vous utilisez ce code dans une fonction (fonction que vous voulez utiliser dans une Promise, �videmment) :

    Code : S�lectionner tout - Visualiser dans une fen�tre � part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    function any() {
     return getDataPromise()
        .then(
            data => { // resolve()
                console.log("Process 1:", data);
                return getDataPromise();
            },
            error => { // reject()
                console.log(error);
            })
        .then(
            data => { // resolve()
                console.log("Process 2:", data);
                return getDataPromise();
            },
            error => { // reject()
                console.log(error);
            })
        .then(
            data => { // resolve()
                console.log("Process 3:", data);
            },
            error => { // reject()
                console.log(error);
            })
    }
    ;
    Il faut bien penser � faire un RETURN de la promise dans la fonction, sinon, vous lancez votre promise en asynchrone, car elle ne sera pas dans la cha�ne des renvois...

    A noter aussi que la fonction Promise.resolve(MaFonction/Ma variable) est tr�s utile, elle permet de renvoyer une Promise sur une autre fonction ou m�me sur une variable, m�me si elles ne sont pas elle m�me des Promise. Ce qui permet de continuer une cha�ne (ou composition dans l'article) m�me avec du code qui n'est pas de type Promise.
    Mis � jour 04/07/2017 � 19h08 par blbird
  2. Avatar de yahiko
    • |
    • permalink
    Merci pour cette contribution
  3. Avatar de k.marwen
    • |
    • permalink
    ca faisait longtemps que je cherchais un tuto qui expliquait simplement les promise
    je l'ai trouv� ici, Merci pour ces explications