Depuis une dizaine d’années et l’avènement d’AJAX, les développeurs web s’appuient sur des fonctions asynchrones afin de construire des pages et applications dynamiques.

Asynchrone : Qualifie des mouvements qui se font de manière décalée dans le temps.

On peut observer ces dernières années une tendance à baser les nouveautés du langage Javascript sur des modèles asynchrones — c’est le cas pour IndexedDb et getUserMedia par exemple — mais aussi à adapter des fonctions synchrones existantes en fonction asynchrones (comme localStorage avec localForage).

async all the thingsPourquoi utiliser des fonctions asynchrones ? Car cela permet entre temps au moteur Javascript de gérer d’autre tâches (événements, affichage, interrogation de base locale, requêtes AJAX, …) et de conserver une interface réactive.

Bien que ces fonctions soient très utiles, elles demandent cependant une gestion différente des fonctions synchrones. En effet, on ne peut pas simplement retourner une valeur depuis une fonction asynchrone.

Pour illustrer cette notion, prenons un exemple issu du Guide du voyageur galactique de Douglas Adams.
Des chercheurs construisent un supercalculateur, Pensée Profonde, afin de calculer la réponse à la grande question sur la vie, l’univers et le reste.
Après 7 millions et demi d’années, Pensée Profonde peut enfin fournir sa réponse :

Voir l’exemple Pensée Profonde par Nicolas Chevobbe (@nchevobbe) sur CodePen.12994

Nous pouvons considérer Pensée Profonde comme un serveur, vers lequel nous envoyons une requête AJAX.
Ce que nous faisons ici, c’est que nous interrogeons Pensée Profonde, nous désactivons le bouton et indiquons que le calcul est en cours.
Puis, lorsque Pensée Profonde retourne la réponse, nous l’affichons.

Pour récupérer la réponse de Pensée Profonde, nous sommes obligés de passer en paramètre de la fonction get_reponse une autre fonction, qu’on appelle callback, qui sera lancée depuis get_reponse avec en paramètre de ce callback la réponse obtenue.

Étant donné la complexité de la question, imaginons que Pensée Profonde soit parfois en surchauffe, et qu’il doive redémarrer.

Voir l’exemple Pensée Profonde par Nicolas Chevobbe (@nchevobbe) sur CodePen.12994


Essayez plusieurs fois pour arriver à déclencher une surfchauffe

Pour gérer cela, dans notre code d’appel nous devons passer une deuxième fonction, failureCallback, qui sera lancée si un problème survient dans get_reponse :

Problème

Avec ce fonctionnement, ce n’est pas la fonction appelante qui est responsable du traitement exécuté une fois la tâche asynchrone terminée.
Il revient à la fonction get_reponse de lancer le callback, au bon moment, le bon nombre de fois, avec la réponse en paramètre; ou le failureCallback si un problème est survenu.
Au sein d’une équipe, vous n’êtes peut-être pas en charge de l’écriture de cette fonction, ou plus dangereux, peut-être que le callback devra être déclenché par un service tiers hors de votre entreprise.

Que se passe-t-il si le callback est appelé plusieurs fois ? Si à la fois le callback et le failureCallback sont appelés ?

Dans notre cas, cela n’est pas grave, nous ne faisons qu’afficher une donnée. Mais imaginez que votre callback soit une fonction qui finalise un achat et lance un paiement. N lancements = N paiements, pas sûr que le client soit satisfait.

ooops

On peut bien évidemment s’assurer que cela ne se produise pas dans le code appelant, en utilisant des variables flags que l’on passerait à true une fois un callback appelé :

Cela fonctionne, mais est loin d’être élégant et complexifie la lecture et la compréhension du code.

Il existe un objet qui va nous permettre de gérer ce genre de cas : Les Promises

Promise

Définition

Une Promise est un objet, introduit par la nouvelle version de Javascript ES6 (pour EcmaScript 6, aussi appelée ES2015), qui est la représentation d’une valeur future.
c’est-à-dire une valeur qui n’est pas forcément connue au moment de la création, ce qui peut assez bien représenter une requête Ajax par exemple, mais aussi une récupération de données dans une base IndexedDb.
D’une manière générale, les Promises vont nous permettre de nous affranchir des callbacks et de l’inversion de contrôle qu’ils induisent (c’est la fonction appelée qui est chargée de les lancer).

L’objet Promise contient des méthodes qui permettent de définir les traitements à effectuer une fois que la tâche, asynchrone ou non, n’est plus en attente.

Une Promise peut avoir différents états:

  • pending (en attente) : l’état initial
  • resolved (tenue) : l’opération a réussi
  • rejected (rompue) : l’opération a échoué

On pourrait comparer les Promises aux listeners sauf que :

  • la Promise ne peut réussir ou échouer qu’une seule fois
  • si la Promise a réussi, et que vous assignez un callback après la réussite de celle-ci, le callback sera quand même appelé. Alors que si vous déclarez un listeners pour un événement déjà réalisé, votre callback ne sera jamais appelé.

Examinons la syntaxe d’une Promise, en gardant notre exemple de Pensée Profonde :

Et pour récupérer la valeur de cette fonction :

Si l’on s’en tient à cette syntaxe, les deux méthodes se ressemblent encore très fortement.
Cependant, si pour une quelconque raison on décidait de résoudre la Promise deux fois, voire de la résoudre puis de la rejeter, le traitement, lui, ne serait déclenché qu’une seule fois.

Chaînage

Les Promises vont aussi nous permettre d’optimiser le code et sa lecture.
Nos fonctions de gestion du succès et de l’échec se ressemblent finalement beaucoup : dans les deux cas on affiche un message puis on réactive le bouton.
Avec les Promises, on pourrait transformer cela en :

Que se passe-t-il ici ? On chaîne plusieurs fonctions à la suite :

Dans notre cas, nous chaînons une fonction synchrone à une fonction asynchrone, mais la puissance de ce mécanisme se révèle quand on chaîne plusieurs fonctions asynchrones entre elles.

Imaginons que nous souhaitions par exemple afficher trois chapitres d’une nouvelle.
Chaque chapitre pouvant être assez conséquent, nous décidons d’avoir une fonction permettant d’effectuer un appel AJAX et de récupérer un chapitre.
Cependant, nous voulons tout de même afficher les chapitres aussi rapidement que possible, et dans le bon ordre bien sûr.

Attente de plusieurs traitements asynchrones simultanés

Là où les Promises deviennent vraiment utiles c’est dans la gestion de traitements “parallèles”, qu’ils soient asynchrones ou non.
Prenons un cas classique d’une application qui pour être fonctionnelle a besoin d’effectuer plusieurs appels au serveur pour récupérer des données de la base.

Dans notre cas, on ne veut afficher la liste des personnes qu’une fois les différentes villes chargées.
Avec des callbacks, on aurait pu faire :

Vu que les appels ne sont pas liés, on pourrait les lancer en parallèle :

On utilise là aussi des flags pour vérifier ce qui a été récupéré, ainsi que des variables pour stocker les valeurs.
Nous n’avons que deux fonctions, imaginons avec dix, cela serait déjà beaucoup plus contraignant.

Les Promises permettent de gérer ce genre de cas avec Promise.all.
Promise.all prend en paramètre un tableau de Promises, puis retourne une Promise, sur laquelle on pourra déclencher une fonction de succès ou d’échec.

Nous avons ici un mécanisme totalement clair et un code très lisible sur ce qui doit être chargé.

Pour autant, les Promises dans Promise.all peuvent avoir leurs propres fonctions gestionnaires :

Ce mécanisme peut se révéler très utile aussi quand le nombre de services à appeler est variable.

Ici, même si needUsers et needCities sont à false, le callback dans then sera quand même appelé.

Gestion erreur

Nous avons vu précédemment que pour gérer le reject, nous passions une fonction en deuxième paramètre de then.

Mais il faut savoir que failureFn est aussi appelée si une exception est lancée dans successFn.

Cela offre un mécanisme de gestion d’erreur intéressant, mais peut se révéler problématique.
Prenons le cas suivant :

À votre avis, qu’est-ce qui est affiché dans ce cas ?

Rien.

L’erreur se trouvant dans la fonction du then, la fonction d’erreur n’est pas appelée, l’exception Javascript est automatiquement interceptée et ne remonte pas dans la console.

Pour cela, il faut chainer une autre fonction, catch.

Le catch captera n’importe quelle erreur ou reject déclenché dans l’ensemble des fonctions déclarées avant lui.

On peut aussi directement se passer de la failureFn :

Cela permet de gérer facilement des erreurs dans toute la chaîne d’appel de fonctions.

La gestion des erreurs peut s’avérer délicate, avec un catch quasimment implicite. Cependant, pour les cas où le catch n’est pas déclaré, les outils de développement des navigateurs s’améliorent.

Ainsi dans la dernière version de Chrome, notre code afficherait :

Dans Firefox, cela ne se fait pas automatiquement, mais on peut garder la Promise dans une variable et l’afficher dans un log.

On a d’abord l’affichage du bon déroulement de la Promise :

Puis 5 secondes plus tard, l’erreur :

Cela n’est évidemment pas parfait, mais les outils vont sûrement s’améliorer. D’ailleurs Chrome est déjà en train de prévoir un module spécifique pour recenser l’ensemble des Promises de l’application et leurs états (cf DevTools : State of the union 2015 – Addy Osmani).

Exemple d’utilisation

Avec les différents mécanismes décrits ci-dessus, on peut penser à des cas d’utilisation multiples.

Nous avons notamment eu le cas sur un de nos projet récemment. Afin de calculer une valeur par rapport à des données saisies par l’utilisateur, nous effectuons une requête Ajax vers le serveur, qui interroge la base de données et renvoie le resultat. Pendant ce traitement, nous affichons un masque de chargement sur le composant, que nous cachons une fois la réponse obtenue.

Cela était satisfaisant, mais parfois, le traitement était si rapide que le masque ne s’affichait qu’une fraction de seconde, résultant en un « flash » de l’écran assez perturbant pour l’utilisateur.
Ce traitement pouvant être néanmoins relativement long, il nous était indispensable d’afficher ce masque.
Pour éviter le flash, nous pensions afficher le masque au minimum 1 seconde.
En utilisant une méthode classique, le code ressemblait à cela :

On joue avec deux flags et une fonction de vérification pour s’assurer que tout soit fini et cacher notre masque.
La même chose avec les Promises :

D’un point de vue fonctionnel, nous avons la même chose, mais la lecture du code s’en trouve facilitée.

Futur

Les Promises permettent une gestion native et puissante de l’asynchronicité, ce qui permet une lecture de code simplifiée.
Cela ira encore plus loin dans ES7 (pour EcmaScript 7), avec les fonctions async.

Reprenons une de nos fonctions précédentes :

Avec async, notre fonction get_reponse resterait la même, mais le code appelant pourrait ressembler à :

On déclare une fonction asynchrone avec async, et on appose à la récupération de notre Promise le mot clé await.
Cela va mettre en pause la fonction jusqu’à ce que la Promise soit terminée, et on pourra alors utiliser dans la suite du traitement la réponse de notre Promise.
Si jamais la Promise est rejetée, une exception est lancée, que l’on peut gérer avec un try/catch.

Cela simplifie encore plus la lecture du code asynchrone, en le rapprochant de ce que l’on peut trouver dans du code synchrone.

ES7 est encore à l’état de discussion et est loin d’être présent dans nos navigateurs, mais vous pouvez utiliser dès aujourd’hui ces fonctions en utilisant un transpilateur comme Babel (anciennement 6to5), qui se chargera de transformer le code en quelque chose de compréhensible pour les navigateurs ES5.

Conclusion

Les Promises permettent de gérer de manière élégante l’asynchronicité qui est si présente dans les applications modernes. Elles sont la base pour de futurs mécanismes qui simplifieront notre code et sa compréhension, et sont déja très répandues dans le monde de NodeJS.

Les Promises sont disponibles nativement pour Chrome (32+), Firefox (29+), IE11, Safari 7.1, et peuvent être utilisées dans les autres navigateurs via des librairies ou polyfill (exemple : https://github.com/getify/native-promise-only, qui respecte strictement les spécifications).
DON’T PANIC, les Promises promettent un futur asynchrone radieux.

Ressources

https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Promise
http://www.html5rocks.com/en/tutorials/es6/promises/
https://www.youtube.com/watch?v=wupXZp5khng
http://javascriptplayground.com/blog/2015/02/promises/
http://www.htmlxprs.com/post/48/understanding-async-functions-in-es7