Monthly Archives: février 2015

L’asynchronisme en JavaScript

Quiconque a déjà programmé en JavaScript, s’est déjà confronté aux notions de programmation asynchrone. Voyons 3 façons d’appréhender la problématique.

Que signifie « Asynchrone »

Plutôt que d’avoir une valeur retournée directement par votre fonction, vous passez une callback qui sera appelée lorsque le résultat sera disponible. Par exemple, lors d’un appel AJAX, durant le traitement du serveur, le thread JavaScript n’est pas bloqué en attendant le retour. Il traite d’autres instructions. Si ce n’était pas le cas, la page web serait paralysée pendant toute la durée du traitement serveur. L’idée est donc de ne pas bloquer le thread principal dans l’attente de la réponse et de fournir une fonction qui sera appelée lorsque le résultat sera disponible. Un message est mis dans la queue pour être traité une fois la réponse reçue. Ainsi, le navigateur peut continuer ses traitements pendant que l’appel HTTP est toujours en cours.

De par sa nature monithread, en JavaScript les I/O sont non bloquantes. Cela signifie qu’il n’y a pas d’attente de la réponse. Le code continue de s’exécuter. Le traitement de retour est positionné dans une callback qui sera exécutée quand la réponse surviendra.

Déroulement d’un appel AJAX:
1 – lancement de la requête AJAX
2 – Enregistrement d’une callback de retour dans la queue
3 – Traitement d’autres messages de la queue
4 – Exécution de la callback quand la réponse est arrivée. La stack doit bien évidement être vide pour traiter la réponse.

Exemple :

makeAjaxRequest("http://www.google.fr", function(response) {
    console.log("Retour appel ajax");
});
console.log("Fin");

Sortie de la console :

Fin
Retour appel ajax

On voit bien que la première instruction console.log n’est pas la première exécutée.

 

Patterns asynchrones

Callback

En JavaScript, les fonctions sont d’ordre supérieur c’est à dire qu’elles peuvent prendre une ou plusieurs fonctions comme entrée et renvoyer une fonction comme retour. De la première caractéristique découle qu’il est possible de passer une fonction en argument d’une autre et de l’exécuter plus tard dans le code. C’est le principe même des callbacks.
Une fonction de callback est une fonction passée en paramètre d’une autre fonction et appelée à l’intérieur de cette dernière.

Le callback hell est la complexité qui apparait lorsque l’on essaie de séquencer du code asynchrone utilisant des callbacks. Il en résulte du code imbriqué, difficile à lire et à maintenir. Le code ne se lit pas verticalement, la dernière ligne n’est pas la dernière instruction mais la plus profonde.

Exemple de callback hell aka pyramid of doom.

function1(args, function () {
    function2(args, function () {
        function3(args, function () {
            function4(args, function () {
                console.log('Done!');
            });
        });
    });
});

Le code est péniblement refactorable. Imaginez que vous souhaitiez inverser les fonctions 2 et 3 – et pour un peu plus de réalisme elles n’ont pas un nom similaire ni les mêmes arguments. Il vous faut coupez/coller la fonction 3 au dessus de la 2, réimbriquer le code et le réindenter. C’est assez laborieux.
Il est possible de nommer et d’extraire les fonctions, mais cela ne résout pas le problème

En nommant les fonctions de callback nous arrivons à quelques chose de mieux. La profondeur du code est moindre, mais il est tout de même difficile d’avoir une vue globale de l’enchainement des fonctions. Il faut naviguer de fonction en fonction pour découvrir le séquencement des appels.

function1(args, callback1);

function callback1() {
    function2(args, callback2);
}

function callback2() {
    function3(args, callback3);
}

function callback3() {
    function4(args, callback4);
});

function callback4 () {
    console.log('Done!');
});

 

Promesse

Le but des promesses est de simplifier l’écriture de code asynchrone. Les API utilisant les promesses ne prennent pas de callback en paramètre mais renvoient un objet promesse. Un objet promesse n’a que quelques méthodes, dont la plus importante est then. La méthode then prend un ou deux arguments, le premier est appelé quand la promesse est résolue (succès) et le second lorsqu’elle est rejetée (erreur). Il est possible de chainer les promesses grâce à la méthode then. Pour cela, la fonction de traitement peut renvoyer une promesse ou une valeur brute qui sera passé en paramètre de la prochaine fonction.
Je ne rentre volontairement pas plus dans le détail car il existe de nombreux articles expliquant le fonctionnement des promesses.

Récrivons l’exemple précédent avec des promesses.

function1(args)
  .then(callback1)
  .then(callback2, callbackError2)
  .then(callback3)
  .then(callback4);

function callback1() {
    return function2(args);
}

function callback2() {
	return function3(args);
}
function callbackError2() {
	console.log("Error2");
}

function callback3() {
	return function4(args);
});

function callback4 () {
	console.log('Done!');
});

Le séquencement est immédiatement perceptible en regardant les « then ». La gestion des erreurs est également simplifiée. Si une promesse est rejetée, l’erreur se propage dans tout les then gérant une erreur.

Generator

Pour une présentation détaillé des générator, je vous invite à lire la présentation très complète de .

En synthèse, les générators arrivent dans ES6 et ne sont donc pas encore supportés par tous les navigateurs. Un générator est une fonction préfixée par le symbole « * ». La fonction est alors en attente jusqu’à ce qu’un appel à la méthode « next » soit effectué. Chaque appel à « next » exécute la fonction jusqu’au prochain « yield »

Exemple :

function simpleGenerator(){
  yield "first";
  yield "second";
  yield "third";
  for (var i = 0; i < 3; i++) {
    yield i;
  }
}

var g = simpleGenerator();
console.log(g.next()); // { value: "first", done: false }
console.log(g.next()); // { value: "second", done: false }
console.log(g.next()); // { value: "third", done: false }
console.log(g.next()); // { value: 0, done: false }
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: undefined, done: true }

Il est possible d’utiliser les générators pour créer des iterators ou faire de l’asynchronisme.
L’exemple qui suit est une illustration de ce qu’il est possible de faire. L’article de expose un exemple plus complet et plus robuste.

Exemple simple:

function request(url) {
    // this is where we're hiding the asynchronicity,
    // away from the main code of our generator
    // `it.next(..)` is the generator's iterator-resume
    // call
    makeAjaxCall( url, function(response){ //callback de succès
        it.next( response );
    },
	function(err) { // callback d'erreur
		it.throw(err);
	}
	);
    // Note: nothing returned here!
}

function *main() {
    var result1 = yield request( "http://some.url.1" );
    console.log("Retour appel 1 =>"+result1);

	// Gestion des erreurs pour cet appel AJAX
	try {
		var result2 = yield request( "http://some.url.2?id=" + result1.id );
		console.log( "The value you asked for: " + result2.value );
	}
	catch(e) {
		console.log("Erreur " +e);
	}
}

var it = main();
it.next(); // get it all started

La fonction main est un generator.

  1. it.next() exécute le generator jusqu’au premier yield
    La méthode request est exécutée, l’appel AJAX est donc lancé.
  2. Le generator est en pause.
  3. Le retour de l’appel AJAX déclenche la callback.
    La callback lance la commande it.next() en passant en paramètre les données de retour
  4. Le generator continue jusqu’au prochain yield
    La méthode request est exécutée, l’appel AJAX est donc lancé.
  5. Le generator est en pause.
  6. Le retour de l’appel AJAX déclenche la callback.
    La callback lance la commande it.next() en passant en paramètre les données de retour
  7. Fin

Hormis la présente de yield dans le code, la structure est identique à du code synchrone. Y compris la gestion des exceptions.

Sources

https://msdn.microsoft.com/fr-fr/library/windows/apps/hh700330.aspx
http://www.infoq.com/fr/news/2014/05/promises-javascript
http://www.infoq.com/fr/news/2014/05/promises-javascript
http://javascriptissexy.com/understand-javascript-callback-functions-and-use-them/
http://seajones.co.uk/do-we-need-to-beat-callback-hell/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators

JavaScript concepts clés : monothread et eventloop

Avant de présenter les différents patterns pour gérer l’asynchronisme en JavaScript, il est préférable de connaitre quelques concepts clés comme l’exécution sur un seul thread ou encore l’event loop. Cet article à pour but des les présenter.

Un seul thread pour l’application

JavaScript est un langage monothread, c’est à dire qu’à un instant T, un seul morceau de code s’exécute. On peut considérer, qu’une fonction est un morceau de code indivisible : quand un moteur JavaScript débute l’exécution d’une fonction il finit son exécution jusqu’à la fin (principe de « run to completion »).
Ce modèle possède un désavantage : l’appel d’un processus de longue durée bloque toute exécution tant que ce processus n’est pas terminé. Les éléments d’interface utilisateur cessent de répondre, les animations sont suspendues et aucun autre code de l’application ne peut s’exécuter. Généralement, le navigateur affiche un message de la sorte « Le script met trop de temps à répondre ».

Event loop

Il faut savoir que JavaScript gère la concurrence grâce à une « boucle d’évènements ». Ce modèle est différent de la gestion faite par des langages comme C ou Java. Afin de la comprendre ce qu’est l’event loop, trois parties importantes d’un moteur JavaScript méritent d’être présentées. Bien que l’implémentation des différents moteurs soient différentes, le principe reste vrai pour chacun d’eux.

eventloop1

Heap : La Heap est la partie la plus simple. C’est une zone de mémoire où vivent les objets (variables, fonctions …).

Stack : La stack est l’endroit où les fonctions en cours d’exécutions sont placées. Si une fonction A() lance une fonctin B() il y a deux niveaux dans la stack.
A chaque fois qu’une fonction est ajoutée à la stack elle est appelée « frame ». Les « frames » contiennent des pointeurs vers les fonctions situées dans la heap, vers les objects accessibles dans le scope de la fonction et bien sûr les paramètres d’appel de la fonction.

Queue : file de messages à traiter. Chaque message est associé à une fonction. Lorsque la pile est vide, c’est au tour du prochain message d’être traité. Ce message rempli alors la stack. Le traitement d’un message est fini quand la stack devient vide.
Si vous codez setTimeout(function() { console.log(‘pouet’); }, 10);, cette fonction anonyme sera mise dans la queue.
Aucune fonction dans la queue ne sera exécutée tant que la stack n’est pas vide. C’est pour cela que le deuxième paramètre de la fonction setTimeout assure un temps minimum avant l’exécution
et non pas un temps garanti.

Donnons vie à ce schéma.

 

Sources

https://thomashunter.name/blog/the-javascript-event-loop-presentation/
https://developer.mozilla.org/fr/docs/Web/JavaScript/Guide/Boucle%C3%89v%C3%A9nements