Cet article est une reprise à l’identique de l’article « RxJS pour les humains » de Nicolas Baptiste. Je l’ai trouvé très enrichissant et très bien expliqué. Je me permets simplement d’apporter quelques modifications au niveau de la mise en forme, notamment dans les exemples de code donnés.
Bonne lecture !
Bien malin est celui qui aurait pu prédire l’avenir du Javascript à sa création ! Parent pauvre de la programmation, considéré comme un langage de bidouilleur, il a désormais entièrement sa place dans les front-ends les plus complexes mais aussi côté backend avec NodeJS.
Le langage a évolué ainsi que les outils permettant de le manipuler (frameworks, IDE, outils de débogage, etc.). Et grâce à Angular ou encore Cycle.js on entend de plus en plus parler de RxJS et de programmation réactive. Quelques mots barbares pour nous autres pauvres développeurs obligés il y a encore quelques temps de placer des alert pour déboguer nos programmes.
Avant de rentrer directement dans le vif du sujet, remontons un peu le temps pour comprendre comment nous en sommes arrivés là et surtout à quelle problématique répond la programmation réactive.
Prenons un bout de code tout simple :
var foo = 'a';
if (foo === 'a') {
console.log('foo = a');
} else {
console.log('foo != a');
}
alert('Voilà tout est fini');
On commence tout doucement.
Le code s’exécute ligne par ligne, chaque instruction est bloquante et si, pour une obscure raison, l’exécution de la fonction console.log prenait 3 secondes, l’instruction alert s’exécuterait après 3 secondes.
Jusque-là tout va bien. Et puis un beau jour vint le code asynchrone avec des termes comme xhr et AJAX.
var req = new XMLHttpRequest();
req.open('GET', 'http://www.mozilla.org/', true);
req.onreadystatechange = function (aEvt) {
if (req.readyState == 4) {
if(req.status == 200)
dump(req.responseText);
else
dump("Erreur pendant le chargement de la page.\n");
}
};
req.send(null);
Source: https://developer.mozilla.org/fr/docs/Web/API/XMLHttpRequest
On instancie un objet de type XMLHttpRequest qui permet de requêter une adresse. Puis à l’aide d’une astucieuse fonction enregistrée via onreadystatechange, notre bout de code est exécuté lorsque le réseau daigne nous répondre.
Ce n’était déjà pas mal, mais la syntaxe n’est pas très jolie. En plus, la version d’Internet Explorer de l’époque ne proposait qu’un ActiveXObject et il fallait en tenir compte car c’était de loin le navigateur le plus utilisé.
Puis il y eu la révolution jQuery ! Ah, le sentiment de puissance et de facilité avec $.ajax ou même $.get ! Toute la difficulté d’instanciation de nos requêtes asynchrones était masquée dans une API relativement simple : http://api.jquery.com/jquery.ajax/
$.get('ajax/test.html', function (data) {
$('.result').html(data);
alert("Load was performed.");
});
Source: http://api.jquery.com/jQuery.get/
En 4 lignes de code c’est plié ! Un appel, puis une fonction de rappel (callback en anglais) est exécutée si l’appel aboutit, une autre si l’appel échoue.
Mais très vite, nous sommes tombés sur ce que l’on appelle : LE CALLBACK HELL ! Un nom qui fait très peur pour décrire une fonction de rappel faisant un autre appel asynchrone qui déclarait lui-même un autre rappel qui faisait lui-même un autre appel asynchrone et ainsi de suite…
Regardons cet exemple en NodeJS :
fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err);
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename);
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err);
} else {
console.log(filename + ' : ' + values);
aspect = (values.width / values.height);
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect);
console.log('resizing ' + filename + 'to ' + height + 'x' + height);
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function (err) {
if (err)
console.log('Error writing file: ' + err);
});
}.bind(this));
}
});
});
}
});
Source: http://callbackhell.com/
Je n’ai même pas envie de décrire ce bout de code. Il est horrible à lire et encore plus dur à déboguer en cas de problème. Mais que celui ou celle qui n’a jamais codé de la sorte jette la première pierre !
Mais un (autre) beau jour, une solution élégante à ce problème est apparue : les promesses.
On peut citer notamment la librairie Q de Kris Kowal. Il le dit lui-même, sa librairie peut gérer ce qu’il appelle the Pyramid of Doom.
step1(function (value1) {
step2(value1, function (value2) {
step3(value2, function (value3) {
step4(value3, function (value4) {
// Do something with value4
});
});
});
});
devient:
Q.fcall(promisedStep1)
.then(promisedStep2)
.then(promisedStep3)
.then(promisedStep4)
.then(function (value4) {
// Do something with value4
})
.catch(function (error) {
// Handle any error from all above steps
})
.done();
Le paradigme des promesses est le suivant, par exemple : j’envoie ma petite sœur chercher du pain, puis quand elle reviendra je le découperai puis je me beurrerai une tartine.
Je prévois toute ces actions à l’avance mais je ne pourrai les effectuer qu’à son retour. En attendant je vais aller faire un peu de jardinage !
Si on code ça dans AngularJS par exemple (qui implémente sa version des promesses via le service $q), cela nous donne :
function sendLittleSisterToFetchTheBread() {
var deferred = $q.defer();
var plombes = 1000;
setTimeout(function () {
deferred.resolve('bread');
}, 3 * plombes);
return deferred.promise;
}
sendLittleSisterToFetchTheBread()
.then(function (bread) {
console.log('I cut the ' + bread);
return 'toast';
})
.then(function (toast) {
console.log('I use the ' + toast + ' to put my butter on it');
});
workInTheGarden();
Ainsi, toute la logique permettant à ma petite sœur d’aller chercher le pain, puis ensuite d’utiliser le pain est mise en place de manière élégante et lisible. Je n’ai donc pas à attendre 3 plombes pour travailler dans le jardin.
Avec les promesses on résout beaucoup de cas fonctionnels en produisant un code assez propre. Mais forcément, on en veut toujours plus. Par exemple : comment dire à ma petite sœur, qu’en fait, du pain il y en avait déjà dans le congélateur ? La petite n’ayant pas de téléphone portable, impossible de lui dire d’arrêter sa course. Et oui, une promesse ne peut pas être annulée.
Ou encore, une promesse résolue ne peut pas être réexecutée si elle a échoué… Une promesse effectue une action asynchrone, mais comment faire pour réagir à plusieurs événements asynchrones ?
La programmation réactive à notre secours
Aujourd’hui, lorsqu’on parle d’asynchrone, on pense très vite à un appel HTTP vers une ressource externe (le traditionnel AJAX). Mais il s’agit simplement de réagir à un événement qui se produira à un moment donné. De la même manière nous pouvons réagir à tout autre événement sans savoir quand il se produira (click, input, websocket, etc.).
Maintenant imaginez un code qui calcul le nombre de clics d’un utilisateur : facile, une variable initialisée à 0 qui s’incrémente à chaque callback enregistré sur l’événement onclick.
Ensuite imaginez un code qui calcule le temps en millisecondes entre deux clics et qui se met à jour en fonction du dernier clic. On fait moins les malins là. On imagine deux variables dont on calculerait la différence de timestamp, puis qu’on intervertirait dès qu’un nouveau clic arrive… C’est faisable, mais le résultat ne serait pas très beau.
Et là RxJS vient à notre rescousse. Cette libraire va nous permettre de traiter l’ensemble de ces événements comme un flux (en anglais stream) qui s’apparentera ni plus ni moins à un tableau. Et cerise sur le pompon, elle nous offrira tout un tas de méthodes pour traiter ce flux, le filtrer, le transformer ou même le combiner avec d’autres flux !
fromEvent(document, 'click').pipe(
map(() => Date.now()),
pairwise(),
map(([before, after]) => (after - before))
).subscribe(console.log);
Pour chaque click, on retourne un timestamp, puis on groupe chaque timestamp par 2 avec pairwise. Une autre fonction map (destructurée via la syntaxe ES6) calcule la différence entre les 2 timestamps et enfin via subscribe, on redirige la sortie vers la console.
Et voilà ! Avec RxJS et ES6 on résout ce problème en 5 lignes de code parfaitement claires.
Penser en termes de flux / stream
Cette partie est une traduction de l’excellent article ‘The introduction to Reactive Programming you’ve been missing‘ https://gist.github.com/staltz/868e7e9bc2a7b8c1f754 par Andre Medeiros et http://andre.staltz.com
Un flux est une séquence d’événements en cours, ordonnés dans le temps.
Un flux peut émettre 3 choses :
– des valeurs
– des erreurs
– et un signal de complétion indiquant que le flux est terminé et ne renverra plus de valeurs
Avec RxJS, vous pouvez appliquer des fonctions sur un flux et produire d’autres flux.
Par exemple avec la fonction map (utilisée dans l’exemple plus haut) :
clickStream: ---c-----c---c-----c-------c--->
vvvvv map(c => Date.now()) vvvv
---t1----t2--t3----t4------t5-->
RxJS est une implémentation du design pattern Observer. Les flux sont observables et nous réagissons via des fonctions observers.
Chaud et froid
Certains observables ne produiront aucune valeur s’ils ne sont pas écoutés ou observés via la fonction subscribe. On les qualifie de froids (cold en anglais).
const arrayObservable = from([1, 2, 3, 4]).pipe(
map(x => x * 10),
tap(console.log)
);
// rien ne s'est encore produit
arrayObservable.subscribe();
// affiche en console
// 10
// 20
// 30
// 40
A l’inverse certains sont qualifiés de chauds lorsque des valeurs sont produites même si le flux n’a pas de souscription. C’est le cas lorsque l’on créer un flux pour écouter les clics de l’utilisateur. Des valeurs sont produites même si l’on ne souscrit pas à cet observable (ça semble logique).
En revanche, les observables sont fainéants (lazy en anglais) et ne seront traités, chauds ou froids qu’à l’appel de la fonction subscribe().
Une dernière chose à savoir, est que la fonction subscribe renvoie un objet de type Disposable qui permettra d’arrêter l’écoute d’un flux. Et donc l’arrêt de production de valeurs s’il s’agit d’un observable froid.
Et après ?
Vous commencez à comprendre l’intérêt de RxJS ? Mais comment aller plus loin ? Regardons d’abord comment créer des observables. La librairie nous offre une multitude de méthodes permettant de créer des observables à partir de tableaux, de promesses, d’événements, de callbacks… Et la liste pourrait encore continuer.
Ce qu’il faut ensuite comprendre c’est que l’on résonne désormais en termes de flux et qu’on les manipule pour en obtenir une sortie.
Un très bon site existe pour comprendre l’usage des fonctions de RxJS est http://rxmarbles.com/
Il représente les flux de cette manière :
- une flèche de gauche à droite représente le temps
- des ronds sont des valeurs constituant le flux et renvoyés à un moment précis
- une barre sur la flèche du temps indique lorsque le flux ne renvoie plus de données et que son statut est « complete »
- entre 2 flèches de temps, la fonction utilisée pour transformer le premier flux en un autre flux.
Voyons par l’exemple. Vous connaissez peut-être la fonction reduce qui permet d’accumuler des valeurs de tableau. Avec RxJS, la fonction reduce renverra l’accumulation des valeurs une fois que le flux aura envoyé l’évènement completed.
https://rxmarbles.com/#reduce
Mais si nous voulons produire un nouveau flux B pour chaque valeur que le flux A renvoie, il faudra alors utiliser la fonction scan.
http://rxmarbles.com/#scan
Non désolé je ne suis pas prêt
Que vous le vouliez ou non, le langage JavaScript évolue via les normes ECMAScript. Les promesses sont natives en ES6/ES2015 et les observables sont proposés pour ES2016 ! Plus d’excuses, autant s’y mettre dès maintenant sinon dans peu de temps vous serez complètement largués !
https://github.com/tc39/ecma262
Mais dans la vraie vie je m’en sers comment ?
Pour répondre à cette question, allez chercher des ressources sur internet, il y en a plein ! Vous pouvez commencer par les exemples de la librairie : https://github.com/Reactive-Extensions/RxJS/tree/master/examples
Ou encore, allez voir par ici pour savoir comment intégrer RxJS avec AngularJS ou JQuery : https://github.com/Reactive-Extensions/RxJS/tree/master/doc/howdoi
Pour lire la documentation de manière plus simple : http://xgrommx.github.io/rx-book/index.html
La version 6, est la version stable actuelle : https://rxjs-dev.firebaseapp.com
RxJS est le lodash pour les événements
Ben Lesh, project leader RxJS
Si vous connaissez lodash, vous savez la richesse de cette librairie et le temps qu’il faut pour connaître l’ensemble de ses fonctions. En revanche, il n’est pas nécessaire de tout maîtriser pour commencer à utiliser lodash et il en va de même avec RxJS.
D’autant plus que les Reactive-Extensions peuvent être utilisées dans un tas de langages : .NET, C++, JS, Ruby, Pytho, PHP, Java, Scala, Go, et bien d’autres !
Les langages changent mais la logique reste la même. Alors qu’attendez-vous pour commencer ?