Retour sur la DotJs 2015 - L'enfer des callbacks
Un deuxième retour sur la DotJS 2015, et cette fois-ci, c'est sur l'excellente présentation de Christophe Porteneuve. Pour pouvez d'ailleurs retrouvez ces slides sur ce lien.
Son sujet: "Modern async js", ou comment éviter l'enfer des callbacks et faire un code asynchrone.
Christmas tree effect
En effet, dans le monde JavaScript, pour gérer de l'asynchronisme, la meilleure solution est de passer une fonction en paramètre d'une autre. C'est ce que nous appelons une "callback". Et l'asynchronisme, il peut en avoir beaucoup:
- Un appel Ajax
- Un appel à un WebWorker
- Un accès à une base de données
- Attente d'un traitement long
- ...
Et ainsi, de file en aiguille, dans des applications complexes, nous pouvons arriver à l'effet "Christmas tree":
asncFunction1(function(err, result) { asncFunction2(function(err, result) { asncFunction3(function(err, result) { asncFunction4(function(err, result) { asncFunction5(function(err, result) { // do something useful }) }) }) }) })
Exemple de la librairie Async.js: https://www.npmjs.com/package/asyncjs
Ainsi, nous avons un code:
- Ou les fonctions ont du mal à s'enchainer
- Intrinsèquement, cela induit que la composition de fonction est délicate par ce biais
- La relecture du code est compliqué
- La remontée des erreurs devient compliqué
De plus, et cela dépend évidemment de l'usage en interne qu'est faite cette callback, mais nous ne savons pas si celle-ci sera exécutée, ni si elle sera appelée qu'une seule fois (erreur courante).
Async.js
Ainsi, nous devions utiliser des solutions palliatives. Dans le monde NodeJs, utiliser "EventEmitter" est une solution. Après, si nous voulons vraiment nous faciliter la tâche, nous pouvons utiliser une librairie appelé "Async.js" et qui permet de résoudre un grand nombre de problèmes vu dessus.
async.list([ asncFunction1, asncFunction2, asncFunction3, asncFunction4, asncFunction5, ]).call().end(function(err, result) { // do something useful
});
Exemple de la librairie Async.js: https://www.npmjs.com/package/asyncjs
Promise
Néanmoins, et cela depuis 2007, une solution existe, et elle est devenue un standard dans la norme ES6, c'est l'usage des "Promise". Un excellent site explique même comment les implémenter nous-même: https://www.promisejs.org/
C'est un objet natif que nous trouvons dans les navigateurs récents et dans NodeJs depuis la version 0.11.33 (nous pouvons dire à partir de 0.12.x) et qui:
- Permet d'enchaîner l'appel de fonctions
- Permet de composer des Promises avec d'autres
- Garantie que le callback sera appelé qu'une seule fois
- Remonte les erreurs
- Gérer du code asynchrone
Pour se créer une Promise, il suffit d'en instancier une ou nous aurons à appeler la callback "resolve" ou "reject" en fonction si nous voulons que cette Promise soit une réussite ou non.
function asyncFunc() { return new Promise(function (resolve, reject) { if (Math.round(Math.random()) === 1) { resolve(); // You can pass a value } else { reject(); // You can pass an exception } }); }
Ce qui nous permet d'avoir du code fluide:
getJSON('story.json') .then(function(story) { return getJSON(story.chapterUrls[0]); // Chain injection! }). then(function(chapter1) { addHtmlToPage(chapter1.html); }) .catch(function() { // Exception catching! addTextToPage("Chapter couldn’t display. FML."); }) .then(hideSpinner); // In all case
Extrait des slides de Christophe Porteneuve
ES6 generators
Des reproches sont faits néanmoins sur les Promises, notamment une "lenteur" (qui n'existe plus, car l'objet est dorénavant natif) mais aussi difficile à appréhender. Car les exemples que nous avons vu n'est qu'une surface, mais il est vrai (pour les utiliser depuis longtemps) que les Promises peuvent être difficile à aborder. Je vous conseille cet article pour mieux discerner les patterns et les anti-patterns autour des Promises: http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
Une autre approche serait d'utiliser de nouveau un concept introduit dans ES6 que sont les "Generators".
Tout comme les Promises, nous devons retourner soit une valeur, soit une exception. Le principe est de "bloquer" les instructions en attentant que le code appelant nous le demande. C'est le principe des "itérateurs", et d'ailleurs l'objet "Iterator" se base sur les "Generators" ES6.
function* fibonacci() { var a = yield 1; yield a * 2; } var it = fibonacci(); console.log(it); // "Generator { }" console.log(it.next()); // 1 console.log(it.send(10)); // 20 console.log(it.close()); // undefined console.log(it.next()); // throws StopIteration (as the generator is now closed)
Extrait de Mozilla Developper: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator
En revanche, comme nous pouvons le constater: le code n'est pas asynchrone. Ce qui est dommage car le code est plus lisible.
Fusion des Promises et Generators: async / await
Ainsi, des personnes la bonne idée d'utiliser à la fois les Promises et les Generators (ou "Control Flow Utopia"). Ce qui permet d'avoir à la fois un code plus lisible et asynchrone. Mais heureusement, ES7 (ou ES2016?) vient à la rescousse avec "async / await".
C'est une notion que je connais du monde C#, où nous avons des instructions qui permet de rendre une fonction asynchrone en une fonction synchrone et vice-versa.
Dans le cas d'une fonction synchrone, la fonction devra retournée soit une valeur, soit une exception. Et si nous utilisons la syntaxe "async", la fonction retournera alors une Promise.
async function syncFunc() { if (Math.round(Math.random()) === 1) { return 'Success'; } throw 'Failure'; }
Inversement, si la fonction asynchrone retourne une Promise, utiliser "await" permet d'attendre la fin d’exécution de la Promise pour aller à la ligne suivante du code.
function syncFunc2() { var result = await asyncFunc(); // ... }
Une autre solution: Reactive Programming
Pour finir, Christophe Porteneuve nous a également évoqué l'usage de librairie comme RxJs pour faire de la programmation réactive et mieux gérer tous les problèmes que nous avons vu ci-dessus. Mais j’essaierai d'y revenir dans un autre article.
Commentaires
Enregistrer un commentaire