Le coin du dév 18 : La programmation asynchrone (en JavaScript)

0

Mais que voilà ? Serait-ce le 18ème épisode du Coin du dév ? Succédant rapidement au précédent (j’espère que vous les avez tous suivi de manière assidue*), nous allons dans cet article aborder le principe de la programmation asynchrone. Nous resterons dans le domaine du Javascript ici, pourquoi aller voir ailleurs quand on est déjà dans un monde fabuleux ?

Une fois remis de cet extraordinaire montage (oui oui, c’est un montage !), nous allons pouvoir débuter.

On commence fort avec les images… Répondons à cette question dont la référence échappe sûrement aux personnes ayant moins de 20 ans.

Plutôt que partir dans une longue explication, prenons un exemple simple :

function sayHello(){

let whatToSay;

window.setTimeout(function(){

whatToSay = « Hello !! »;

}, 1000);

console.log(whatToSay);

}

sayHello()

Au moment de l’appel de sayHello(), que va-t-il s’afficher dans la console ? « undefined » bien entendu, car la valeur de ‘whatToSay’ est définie de manière différée, 1 seconde plus tard.

Cet exemple est extrêmement simpliste, mais il illustre bien le problème. Si on veut l’appliquer à un cas concret (même si afficher « Hello !! » est une grande étape dans la vie d’un développeur), on peut tout simplement prendre le cas d’une requête HTTP :

function getData(){

let results ;

request(“http://myapiurl.com”, function(data){

results = data ;

})

return results ;

}

console.log(getData())

Nous aurons ici aussi « undefined », le résultat de la requête n’étant pas encore disponible au moment de l’affichage du retour de la fonction. C’est à ça que va nous servir la programmation asynchrone.

Nous allons aborder quatre aspects de ce type de programmation : les callbacks, les promises, les « relativement nouveaux » opérateurs async/await, et le nouveau design qui est le Reactive Programming. Il serait possible de consacrer un article très long sur chacun de ces sujets, nous allons ici tenter de présenter de manière concise et précise chaque élément.

Petite note avant de commencer : certains bouts de code présents dans cet article seront écrits en utilisant des syntaxes spécifiques à l’ES6 (ES 2015) ou ES7 (ES 2016). Afin de pouvoir faire fonctionner de manière sure tous ces snippets, vous pouvez jeter un œil à BabelJS (https://babeljs.io/), qui les convertira en ES5, interprété partout (comme Eurocard Mastercard).

C’est parti !

Les callbacks :

Les plus anglophones d’entre nous comprendront qu’il s’agit d’une fonction de rappel. Non, je ne vous parle pas de la fonctionnalité révolutionnaire de l’iPhone 8, mais d’une méthode permettant d’effectuer une opération après la réalisation d’un autre traitement. En gros, c’est un peu comme quand vous laissez votre numéro au garagiste pour qu’il vous rappelle une fois sa tâche accomplie, vous demandant une somme supérieure au prix de votre véhicule…

En Javascript, ce système est utilisé de manière omniprésente, peu à peu remplacé par les promises (nous y reviendrons ultérieurement) dans certains cas.

Plutôt que de longs discours, voici un exemple très simple de callback :

function myCallback ( data ) {

console.log( data );

}

function compute ( callback ) {

window.setTimeout( function() {

callback( “hello” ) ;

}, 2000 )

}

compute( myCallback ) ; // “hello” après 2s

Ici, même la fonction directement passée au ‘setTimeout’ est un callback.

Un callback est tout simplement une fonction, que l’on passe en paramètre d’une autre fonction. Cette dernière l’exécutera afin de propager le résultat de son traitement et ainsi réaliser les actions voulues.

Dans un cas concret, ceux ayant eu affaire à NodeJS connaissent très très bien les callbacks (voire un peu trop). Prenons l’exemple de l’ouverture d’un fichier :

var fs = require(‘fs’);
fs.readFile( “/etc/shadow”, function (err, data) {
if (err) {
throw err;
}
console.log( data.toString() );
});

La fonction ainsi passée en paramètre sera exécutée une fois le fichier chargé. Un autre exemple extrêmement courant de l’utilisation des callbacks est dans le cas des événements, lors de la déclaration de l’écoute d’un événement, on passe également une fonction qui sera appelée à chaque occurrence de l’événement.

element.addEventListener (« click », function(){ alert(« UN CLIC OMG »); });

Attention, l’abus de callbacks est dangereux pour la santé. Rapidement, le code peut devenir complexe, illisible, et surtout, le changement successif de contexte peut s’avérer problématique.

Les Promises :

Les Promises permettent de réaliser des opérations asynchrones de manière plus simple. Une Promise représente une opération non achevée, dont le résultat est attendu ultérieurement.

Pas de panique, ami lecteur, laisse-moi t’expliquer !

Une Promise se déclare comme tel :

new Promise( (resolve, reject) => {

} );

Une Promise prend en paramètre une fonction, cette dernière ayant elle même deux arguments : ‘resolve’ et ‘reject’.

Soyons organisés dans notre explication.

« Quand j’écris ça, qu’est ce que j’ai au final ? »
En déclarant une Promise, on obtient un objet représentant justement cette promesse. Oui, je sais, on ne s’y attendait pas. Cette promesse est un lien vers le futur résultat, si celle-ci est tenue, ou bien vers une éventuelle erreur, si celle-ci est brisée (même si ce n’est pas bien de ne pas tenir ses promesses).

« Que représente la fonction passée en paramètre de la Promise ? »
C’est un callback ! Celui-ci prend deux arguments : ‘resolve’ et ‘reject’. ‘resolve’ permettra de signaler que la promesse est tenue et ainsi renvoyer le résultat, alors que ‘reject’ annoncera l’échec de celle-ci, renvoyant éventuellement une erreur.

Voyons ceci avec du code :

new Promise( (resolve, reject) => {
window.setTimeout( () => {
resolve(“Hello !!”);
}, 2000);
});

On est d’accord, ce n’est pas le code de l’année, mais ça fait toujours plaisir quand quelqu’un nous dit bonjour. Ici, un objet promis est créé, et la promesse sera tenue ultérieurement, le résultat pouvant être récupéré de la manière suivante :

function myPromise() {
return new Promise( (resolve, reject) => {
window.setTimeout( () => {
resolve(“hello”);
}, 2000);
}) ;
myPromise().then( result => { console.log(result) ; }) ;

Résultat :

Note : les images sont utilisées pour capter l’attention de l’utilisateur. L’exécution du code illustré ci-dessus affichera ces messages exclusivement à la console (rien ne sera rendu visible dans votre navigateur).

 « C’est quoi cette fonction then() qui sort de nulle part ? »

La fonction then() est appelée lorsque la promesse est évaluée (soit tenue, soit rejetée). Ici, nous passons un callback (ils sont partout…) à la fonction afin d’exécuter des instructions une fois le résultat de la promise connu. Ce callback prend en paramètre le résultat de la promesse, ce qui explique le fait que le paramètre ‘result’ corresponde au ‘hello’ qui est donné au ‘resolve’.

Mais ce n’est pas tout !! (OMG PLOT TWIST ! )

Il est possible de chaîner les promises. En effet, cher lecteur, saviez-vous que la fonction then() renvoie elle même un objet Promise, dont la promesse de résultat correspond à la valeur de retour du callback ? Oui ? Ah… Oh, ma phrase ne veut rien dire ? Mais si ! Regardez plutôt le code suivant :

function myPromise (){
return new Promise( function (resolve, reject) {
window.setTimeout( function() { resolve(« hello »); }, 2000);
} );
}myPromise().then(function( result ){
console.log(result);
return « hi. »;
}).then(function( result ){
console.log(result);
return « hi. »;
}).then(function( result ){
console.log(result);
});

Résultat :

Oui, j’aime l’humour de répétition. Puis je n’avais pas envie de trouver une autre image. Disons simplement que c’est pour maintenir les repères que j’ai installé précédemment.

« Bon, c’est bien joli, mais ça sert à autre chose qu’à afficher ce meme de mauvaise facture ? »
Eh bien… Oui ! Les promises sont très répandues désormais. Elles sont implémentées officiellement dans l’ES2015 (ES6), et d’autres librairies permettent un support pour l’ES5 (comme Bluebird, A+, Q …).

Prenons un cas simple dans AngularJS, l’utilisation du service $http :

$http({
url : “myawesomeurl.com”,
method : « GET  // Dans le cas du succès
}, function(response){
// Dans le cas d’une erreur
})

Ceci est une promise, la requête vers « myawesomeurl.com » se fait de manière asynchrone, et la réponse de cette requête sera passé au callback de la fonction then() une fois disponible.

Note : AngularJS dans sa version 1 (la version 2 étant très différente), utilise une implémentation particulière des promises, Q, d’où le $q. Le principe reste cependant le même.

Il est également possible de gérer les erreurs dans une Promise, de la même manière que la fonction then(), il existe la fonction catch(). Celle ci est appelée lors du rejet de la promesse, et permet ainsi d’utiliser un callback spécifique.

 Les async/await :

Note : Les opérateurs async et await sont disponibles dans le ES2016 (ES7).

Voilà donc deux opérateurs. Le principe est très simple : await sert à attendre l’accomplissement d’une opération, et async permet de préciser qu’une fonction se réalise de manière asynchrone.

Il est nécessaire de préciser qu’un await peut uniquement être utilisé sur une fonction déclarée avec async ou bien sur une promise. Voyons ceci par l’exemple :

function myPromise() {
return new Promise( (resolve, reject) => {
window.setTimeout( () => {
resolve(“hello”);
}, 2000);
}) ;
}async function improve() {
let val = await myPromise();
return val + “world” ;
}const value = await improve()
console.log( value ) ; // “hello world”

« Hey, mais c’est à peu près le même bout de code pourri qu’au dessus non ? »
Bien joué, le test de vigilance difficulté 15 est réussi. Vous voyez donc devant vous… UN DEMOGORGON. Wait…

Une fonction async retournera une Promise. Le résultat de cette Promise sera stocké dans la variable « value » au moment de sa résolution. L’utilisation de ces opérateurs permet d’écrire sous une forme synchrone tout un déroulement asynchrone (Cette phrase a inspiré Nolan pour Interstellar !). Cette écriture est bien plus lisible qu’un chaînage de Promises et leur callback.
Attention à ne pas oublier l’opérateur await, auquel cas value serait juste la promise renvoyée par la fonction myPromise().
De plus, la gestion des erreurs est de nouveau assurée par notre bon vieux try/catch :

async function myPromise() {
return new Promise( (resolve, reject) => {
window.setTimeout( () => {
reject(“Order 66”);
}, 2000);
}) ;
}
try{
const value = await myPromise()
catch( error ) {
console.log( error ) ; // “Order 66”
}

Note : Les opérateurs async/await existent dans d’autres langages (Python depuis la 3.5, C#…). Le but de leur utilisation reste similaire, mais la manière de les utiliser peut varier. Nos exemples s’appliquent dans le cadre du Javascript (une piqûre de rappel ne fait jamais de mal, sauf pour les impôts).

Il est intéressant de voir l’évolution de la programmation asynchrone en Javascript. Au début, seuls les callbacks remplissaient cette fonction, de manière simpliste, puis les promises sont arrivées, la délégation étant représentée par un objet complexe, utilisant tout de même les callbacks, puis vinrent enfin les opérateurs, async et await, forme claire et efficace, ces derniers utilisant les promises, mais permettant une écriture légère de tout ce processus asynchrone. Rien n’est refait de zéro et chaque évolution trouve ses fondations dans ce qui existe déjà. C’est beau hein ? Oui, bon, c’était pour faire une petite phrase de conclusion. Retirez donc ce soupir de soulagement, l’article n’est pas terminé. Nous allons maintenant voir brièvement une autre représentation de la programmation asynchrone : la programmation réactive.

 

Le Reactive Programming :

Bon… Le Reactive Programming (ou Programmation Réactive dans la langue de Molière) est une chose complexe, et il sera impossible de tout aborder dans cet article de présentation. Tachons d’être précis.

Le Reactive Programming trouve ses origines dans un design pattern (ou patron de conception dans la langue de Joey Starr) : Observer. Tel le paparazzi guettant le moment ou une star trébuchera sur les marches d’un quelconque festival, ce patron présente un système de notification, où une source notifiera les observateurs enregistrés sur elle lors de l’occurrence d’événements.

Voici un schéma du patron Observable :

(source : https://en.wikipedia.org/wiki/Observer_pattern)

Nous pouvons simplifier le patron en deux objets : l’Observateur et l’Observable. La source est observable par un ou plusieurs observateurs. C’est ceci qui constitue le fondement de la programmation réactive.

Le principe de ce type de programmation est de faire affaire avec des flux de données asynchrones. Afin d’aborder le sujet, il faut considérer que tout peut être un flux, absolument tout. Il sera possible de faire de n’importe quoi un flux observable.

Normalement, à cette étape, vous devriez être dans un des états d’esprit suivants :

  1. « Ouah, mais c’est que l’auteur nous vend du rêve par cargaison là ! »
  2. « Ça y est, il délire, on l’a complètement perdu… »
  3. « Hmm, j’irai bien mangé un bout, cet article m’a donné faim »

Ceux dans le cas n° 1, votre curiosité vous fait honneur. Ceux dans le cas n°2, vous avez sans doute raison aussi… (ou alors, vous êtes mon psy). Concernant le cas n°3, bon appétit.

Je disais donc : tout peut être un flux, un événement de clic, une requête HTTP, … Un flux va pouvoir envoyer trois éléments : une valeur, une erreur, ou un état disant qu’il est achevé. Tous les éléments d’un flux sont ordonnés dans le temps. Nous allons ensuite définir une fonction pour chacun de ces trois cas. Vous l’aurez compris, nos fonctions sont les Observers, et notre flux est l’Observable (ou Subject sur le schéma).

De nombreuses bibliothèques proposent une implémentation du Reactive Programming dans beaucoup de langages. Ici, les exemples seront fait avec RxJS (Javascript, tout ça, tout ça…), mais des extensions existent pour la grande majorité des langages actuels (https://github.com/ReactiveX).

Afin d’illustrer tout ceci, commençons avec un exemple très simple :

observer.next(5);
observer.error(« An error! »);
observer.next(10);
observer.complete();

return () => console.log(‘finish!’)
});

var subscription = source.subscribe(
value => {
console.log(« Value:  » + value );
},
error => {
console.error(« Error:  » + error );
},
() => {
console.log( « Stream done ! complete() called. » );
}
);

// > 5
// > Error: An error!
// > 10
// > Stream done ! onCompleted() called.
// > finish!

Nous déclarons ici un flux (Observable), qui renverra successivement la valeur « 5 », une erreur « An error ! », une valeur « 10 » pour finalement se déclarer achevé.

Note : Dans les anciennes versions de RxJS, next(), error() et complete() sont sous la forme de onNext(), onError() et onCompleted().

Notre source est donc déclarée. Nous allons maintenant enregistrer un Observer pour les valeurs, un pour les erreurs et un pour l’état « completed » du flux.

Voici le principe élémentaire de Reactive.

Après, je comprendrais parfaitement le fait que vous trouviez qu’afficher 5 et 10 ne permet pas de faire grand-chose. Je vous trouve un peu réducteur, mais je le conçois.

Abordons un autre exemple :

var source = Rx.Observable.range(1,15);
var newStream = source.filter(x => x%2 === 0);var subscription = newStream.subscribe(
value => {
console.log(« Value:  » + value ););
console.log(« Value:  » + value ););

Ici, toutes les valeurs paires comprises entre 1 et 15 seront affichées. Mais attention, le flux « source » n’a pas été modifié, un nouveau flux a été créé via l’appel de filter(). C’est ainsi qu’il est possible de faire en sorte d’obtenir les données voulues. Nous pouvons, grâce aux nombreuses fonctions d’Observable créer de nouveaux flux contenant soit des sous-ensemble des valeurs du flux d’origine (via filter()), soit des valeurs modifiées (via flatMap() entre autres), ou encore d’autres possibilités. Il ne faut pas cependant oublier qu’un Observable est immutable. Notre source d’origine sera toujours belle et bien disponible.

Lorsque j’ai commencé à me renseigner sur tout ceci, je me suis dis qu’un Observable est juste une Promise permettant de renvoyer plusieurs résultats. C’est une vision réductrice mais elle permet tout de même d’aborder le principe.

De plus, un Observable n’a pas forcément besoin d’être créé de rien (Ouah la révélation !). RxJS permet de créer des observables depuis des tableaux, des événements, ou même encore des callbacks, des Promises ou de fonctions utilisant async / await (comme quoi, tout est lié, c’est beau !).

Quelques fonctions à la volée :

  • fromCallback()
  • fromPromise()
  • fromEvent()

Vous comprenez mieux le principe du « Tout peut être un flux » ?

De plus, peut-être que vous vous dites que Rx ressemble en de nombreux points à une fonction génératrice (déclaré avec function * () et utilisant l’opérateur yield) ? C’est exact, c’est très similaire. Les Generators sont une fonctionnalité de l’ES6 et reprennent ce principe de flux. Nous déclarons une fonction qui générera un flux de données :

function* fibonacci(){
var fn1 = 1;
var fn2 = 1;
while (1) {
var current = fn2;
fn2 = fn1;
fn1 = fn1 + current;
yield current;
}
}let gen = fibonacci()console.log(gen.next().value) // 1
console.log(gen.next().value) // 1
console.log(gen.next().value) // 2
console.log(gen.next().value) // 3console.log(gen.next().value) // 8
console.log(gen.next().value) // 13
console.log(gen.next().value) // 21

Et, bien entendu, il est possible de transformer un Generator en un observable :

Rx.Observable.from(fibonacci())
.take(10)
.subscribe(function (x) {
console.log(‘Value: %s’, x);
});

Les exemples présentés ici sont effectivement très simples, et c’est volontaire. Ceci n’est qu’une « introduction » au Reactive Programming. Je vous conseille fortement cet article https://gist.github.com/staltz/868e7e9bc2a7b8c1f754 afin de poursuivre.

Conclusion :

Ceci clôture cet article. Comme précisé dés le début, il ne s’agit que d’un « survol », une introduction à la programmation asynchrone. Un article de cette taille pourrait être consacré entièrement à la programmation réactive notamment !

Si vous faites du Javascript, vous en avez forcément utilisé un de ces concepts à un moment ou un autre (hormis les callbacks, ça, vous en avez forcément utilisé des milliers !), que ce soit via des frameworks comme Angular (v1 avec ses promises, v2 avec son implémentation de Rx), ou même en pure JS.

Je vais bien entendu vous mettre à la suite quelques références, dont certaines ont inspiré cet article, afin de poursuivre votre quête, noble aventurier de l’asynchronicité. Que l’opération différée soit avec vous, que vos promesses soient tenues, allez, et accomplissez la prophétie de… euh… la Grande Prophétie.

Les références :

 

Notes :

* Je n’ai jamais dit que les 15 épisodes entre le 2ème article et celui ci existaient.

** Aucune Adèle ou truc qui ressemble à une otarie n’a été maltraité durant l’écriture de cet article.

*** Oui, cet article n’a toujours pas de rapport avec la sécurité.

**** J’aime bien faire une pyramide avec les étoiles.

|          Non, en fait, c’est un demi sapin.

About Author

Jean-Nicolas Piotrowski

Fondateur et Président d’ITrust. Diplômé de l’IUP STRI, ingénieur en télécommunications et réseaux informatiques, il a été successivement Responsable Sécurité de la salle de marché BNP Paribas, consultant sécurité pour la Banque Postale et le Crédit Lyonnais. En 2007, il fonde ITrust et dirige la société.

Leave A Reply