mardi 22 décembre 2015

Petit exercice pour bien comprendre la programmation en javascript


Connaitre la syntaxe du javascript est une chose mais en saisir les subtilités (asynchrone, callback, programmation fonctionnelle)  en est une autre.


L'exercice suivant est synthèse de ce qu'il faut comprendre de la programmation asynchrone.

Prenons l'exemple d'une boucle 'for'


for (var a = 0 ;a < 5 ; a++)
 { console.log("valeur de a sync:" , a); }

L'affichage donnera

valeur de a sync: 0
valeur de a sync: 1
valeur de a sync: 2
valeur de a sync: 3
valeur de a sync: 4

Pas de surprise , ici toutes instructions sont synchrones

Si on introduit la notion d'asynchronisme par le biais d'une temporisation:
(avec une délais de 100 ms et 1 ms)

for (var a = 0 ;a < 5 ; a++)
 { setTimeout(function(){ console.log("valeur de a async 100:" , a);}, 100); }

for (var a = 0 ;a < 5 ; a++) { setTimeout(function()
 { console.log("valeur de a async 1:" , a);}, 1); }

Le résultat sera cette fois plus étonnant:
(extrait)
valeur de a async 1: 5
valeur de a async 1: 5
valeur de a async 1: 5
valeur de a async 1: 5
valeur de a async 1: 5
valeur de a async 100: 5
valeur de a async 100: 5
valeur de a async 100: 5
valeur de a async 100: 5
valeur de a async 100: 5

Deux phénomènes:  un normal (a) et un moins intuitif (b).
a) Les lignes relative à la temporisation la plus faible se présentent en premieres alors qu'elles sont les dernières invoquées.
C'est le principe de l'asynchronisme: Le système n'attend pas le retour de l'instruction (appel de fonction)  pour passer à la suivante.

b) La valeur du compteur est bloqué à 5. 
Dans la cause se niche toute la subtilité d'un système asynchrone: l'appel de la fonction  embarquée dans le timer se fera avec le contexte du moment de l'exécution. A la sortie de la boucle 'for' la valeur de 'a' est 5 et donc les 5 appels de fonction déclenchées par le timer se feront avec un même contexte 'a=5'  

Comment obtenir une sortie conforme à nos  attentes : en utilisant les closures (fermetures).
Une closure permet de conserver le contexte au moment de l'appel de la fonction.

exemple ici :

for (var a = 0 ;a < 5 ; a++)  { 
  setTimeout(function(){ 
    var i = a;
    console.log("valeur de a async 100  avec param :" , i);}, 100);    
}

La variable 'a' est bien déclaré au niveau du 'FOR' et appelée dans le corps d'une fonction imbriquée dans la boucle. Mais le résultat n'est pas probant:

valeur de a async 100  avec param : 5
valeur de a async 100  avec param : 5
valeur de a async 100  avec param : 5
valeur de a async 100  avec param : 5
valeur de a async 100  avec param : 5

Pourquoi ? :  Pour 2 raisons :

a) Les 5 closures partagent le même contexte donc il 'est normal d'avoir 5 fois la même valeur.
b) La fonction est littéralement appelée après la boucle 'for' : il est trop tard, le contexte a été perdu.

La solution: appeler la closure dans le 'for' et avec les 5 contextes 
Pour cela le programme est modifié comme ceci:

for (var a = 0 ;a < 5 ; a++)
 { setTimeout(function(a){ 
   console.log("j appelle le constructeur de fonction avec",a);
   return function () { 
                       var i = a;
                        console.log("valeur de a async 1000 avec closure :" , i);}
    }(a), 1000);
 }
En plus de la closure, on ajout un constructeur de fonction.
Dans le 'for' , on passe en paremètre au timer , non pas une fonction à executer mais une fonction qui retourne une fonction à exécuter. C'est le point fort de ce type de langage de considérer les fonctions comme des données (entier, chaine etc) .

Pour que le contexte soit conservé en l'état pour les 5 itération, la fonction de construction est appelée immédiatement par l'utilisation des  doubles parenthèses et d'un paramètre  à la fin de sa définition " }(a), 1000);".

Le résultat est maintenant correct 

j appelle le constructeur de fonction avec 0
j appelle le constructeur de fonction avec 1
j appelle le constructeur de fonction avec 2
j appelle le constructeur de fonction avec 3
j appelle le constructeur de fonction avec 4
valeur de a async 1000  avec closure : 0
valeur de a async 1000  avec closure : 1
valeur de a async 1000  avec closure : 2
valeur de a async 1000  avec closure : 3

valeur de a async 1000  avec closure : 4

Ce mécanisme est couramment utilisé par jquery, c'est la base d'une programmation correcte en javascript.

Pour aller plus loin :  La magie des closures.

1 commentaire:

Merire a dit…

Bonjour,
Un validateur comme JSLint fera une remarque sur ce code, qui peut avoir un impact sur la performance. Il nous dira : "don't make function within a loop", car dans l'exemple, la première fonction (celle dans le setTimeout, le constructeur) est créée 5 fois sans que cela ne soit utile : c'est la création de la seconde (celle du return) qu'il est important de répéter 5 fois, la closure.

Je suggère donc :
/*jslint
node: true
*/

function constructeurDeFonction(a) {
"use strict";
console.log("j'appelle le constructeur de fonction avec ", a);
return function () {
var i = a;
console.log("valeur de a async 1000 avec closure : ", i);
};
}

var a;
for (a = 0; a < 5; a += 1) {
setTimeout(constructeurDeFonction(a), 1000);
}

Evidemment, dans ce code l'impact est négligeable, mais dans le cas d'une application web à l'heure où l'utilisateur veut tout immédiatement, chaque fraction de seconde peut compter.