Une Callback pour vos fonctions JS asynchrones
Vous est-il arrivé d'utiliser une fonction JavaScript en maudissant son concepteur parce qu'il avait oublié un détail important... vous permettre de faire quelque chose après l'exécution de son code ! Ça m'arrive trop souvent ces derniers temps et ça ne serait pas non plus la fin du monde si les concepteurs n'avaient pas en plus eu la bonne idée de ne fournir que la version minifiée de leur développement ! Merci pour le partage « Dude » ;)
Je vais vous raconter une histoire.
C'est l'histoire d'un gars sympa qui développe un plugin jQuery...
Notre développeur à un besoin. Il estime également que ce besoin est assez générique, que personne n'y a encore pensé et qu'il veut aider son prochain. Notre développeur est très altruiste. Sans plus tarder voici le plugin jQuery qui écris un paragraphe en italique après un autre élément ! Enfin, il faut le développer : alors commençons.
/*** Ce plugin est dépendant de la librairie http://www.jquery.com/ ***/ // Présence du ";" pour ne pas être influancer par les erreurs des scripts précédents. // Création d'une fonction anonyme donnant accès à la variable "$" via son premier paramètre. ;(function ($) { // Ajout d'une fonction "addItalicTextAfter" à l'objet contenu dans "$" dans la liste de fonctions "fn". $.fn.addItalicTextAfter = function () { // Retour d'exécution de chaque élément trouvé par "$('un sélecteur quelconque')". return this.each(function () { // Je crée, je style et je remplis une balise p. var p = $("<p>").css("font-style", "italic").text("J'écris ce texte après mon exemple d'utilisation !"); $(this).after(p); // Et hop, on ajoute ça après la cible courante. }); } // J'exécute tout de suite ma jolie fonction anonyme en lui passant comme paramètre l'objet jQuery (fourni par la librairie jQuery) pour la manipuler via "$" comme prévu dans la dite fonction. })(jQuery);
Notre gars sympa imagine déjà comment le développeur final vas utiliser son plugin sur un élément avec l'id first-example.
$("#first-example").addItalicTextAfter();
...qui permet de changer le texte de la fonctionnalité...
Mais notre développeur ne s'arrête pas là, il permet également de changer le contenu du texte à ajouter !
;(function ($) { // Ajout d'un paramètre d'utilisation pour changer le contenu. $.fn.addItalicTextAfter = function (content) { // Mais si on ne souhaite pas changer le contenu, il y en a toujours un par défaut. if (content == null) { content = "J'écris ce texte après mon exemple d'utilisation !"; } return this.each(function () { var p = $("<p>").css("font-style", "italic") // J'ajoute mon contenu initial ou modifié. .text(content); $(this).after(p); }); } })(jQuery);
Ce qui donne à l'utilisation sur l'id second-example :
$("#second-example").addItalicTextAfter("Et celui-ci après mon autre exemple !");
...qui se soucie de l'aspect et des performances...
Il décide même de permettre la customisation de son rendu : style, classe, balise : il pense ne rien omettre.
Conscient qu'il ne connait pas le nombre d'éléments ciblés et donc le temps de « bloquage » imposé par son script, il va même jusqu'à rendre le traitement asynchrone ! Vraiment sympa ce gars.
;(function ($) { $.fn.addItalicTextAfter = function (content, params) { // Si le développeur ne souhaite pas changer la phrase initiale et qu'il met directement les paramètres... if (typeof content === "object") { // ... "params" est "content"... params = content; // ...et il n'y a pas de "content" au final. content = null; } if (content == null) { content = "J'écris ce texte après mon exemple d'utilisation !"; } // On créé un objet anonyme avec les propriétés style, class et tag dans la fonction extend. // Celle-ci va placer l'objet dans la variable "params" en lui ajoutant les éventuelles propriétés déjà existantes depuis "params" passé en paramètre. // Ça permet de définir des paramètres par défaut écrasés par ceux passés en paramètre de la fonction. params = $.extend({ "style": { "font-style": "italic" }, "class": "", "tag": "p" }, params); return this.each(function () { // Création de $this pour étendre l'utilisation de $(this) dans un setTimeout. var $this = $(this); // A partir d'ici, le code exécuté est asynchrone. // La suite des instructions sera exécuté avant, après, pendant la suite... nous n'en avons aucune idée. // L'avantage est que si le temps de traitement ci-dessous est long, il ne bloque pas notre script, ni même notre page. setTimeout(function () { var // Je crée un tableau de class à ajouter (si plusieurs class sont passées). eachClass = params.class.split(" "), p = $("<" + params.tag + ">") // Je place la balise souhaitée... .css(params.style) // ...avec le style souhaité... .text(content); // ...et le contenu souhaité. // J'ajoute chacune des classes du tableau à mon objet. for (var i in eachClass) { p.addClass(eachClass[i]); } // Le mécanisme précédent pouvait être simplement remplacé par : // p.attr("class", params.class) car il n'est pas sensé y avoir "déjà" des classes sur l'objet créé. // Cependant prenez l'habitude de faire ainsi pour éviter "d'écraser" les classes déjà présentes. // Utilisation de $this car $(this) n'existe plus dans le setTimeout. $this.after(p); }, 0); // Ne perdons pas une seconde à exécuter notre code donc mettons 0 milliseconde de délai. }); } })(jQuery);
Ce qui donne à l'utilisation sur l'id third-example :
$("#third-example").addItalicTextAfter( "Dommage que mon nom de fonction ne veuille plus rien dire !", { class: "warning my-example", style: { "font-weight": "bold", "font-size": "0.8em" } } );
...mais qui oublie de mettre une Callback !
C'est bien beau tout ça. Notre développeur partage donc son œuvre, sans donner le code source (non minifié), sans même mettre en place de support ou un moyen de le contacter. Il oublie même qu'il avait développé son super plugin bien utile.
Mais ce à quoi notre développeur sympa n'a pas pensé, c'est que le développeur utilisateur du plugin veut peut-être rajouter ceci après son code :
$("#fourth-example").addItalicTextAfter("Marche pas...", { class: "my-target like-a" }); // Souhaite permettre une alerte au clique sur l'élément. $(".my-target").click(function () { alert("« Ah bah pas cool :/ » - Christophe L."); }); // Mais sans succès...
FAIL...
La Callback : bonne pratique de développement asynchrone
C'est une énorme problème de la part de notre développeur sympa que de ne pas avoir mis de Callback ou Fonction de rappel ! Ça a rendu des tas de développeurs tristes ! Peut-être que ça a même tué des petits chats.
Dans notre exemple précédent, l'exécution du code est asynchrone ce qui signifie qu'il n'y a aucun moyen de savoir quand le code exécuté sera fini, donc aucun moyen de savoir quand l'élément portant la classe my-target sera ajouté au DOM et donc aucun moyen de savoir si l'évènement onclick ajouté à la suite va cibler un élément ou cibler le vide. Alors on fait quoi maintenant ? On met à notre tour un setTimeout en croisant les doigts pour que nos éléments soient arrivés dans le DOM avant qu'on exécute de quoi leur associer un onclick ? On va voir ça plus loin dans les mauvaises pratiques. Pour le moment : ajoutons notre Callback.
La Fonction de rappel dans une fonction asynchrone
Voici ce qu'on aurait pu faire si nous avions au minima les sources.
;(function ($) { $.fn.addItalicTextAfter = function (content, params, callback) { // Si l'utilisateur tape un "content", mais ne donne pas de "params"... if (typeof params === "function") { // ..."params" est "callback"... callback = params; // ...et il n'y a pas de "params". params = null; } // Si l'utilisateur ne saisit qu'une fonction en premier paramètre... if (typeof content === "function") { // ..."content" est "callback"... callback = content; // ...il n'y a pas de "params"... params = null; // ...et il n'y a pas de "content"... content = null; } else if (typeof content === "object") { params = content; content = null; } if (content == null) { content = "J'écris ce texte après mon exemple d'utilisation !"; } params = $.extend({ "style": { "font-style": "italic" }, "class": "", "tag": "p" }, params); return this.each(function () { var $this = $(this); setTimeout(function () { var eachClass = params.class.split(" "), p = $("<" + params.tag + ">") .css(params.style) .text(content); for (var i in eachClass) { p.addClass(eachClass[i]); } $this.after(p); // Dans le cas ou une Callback a été passé on l'exécute. // On ne sait pas quand elle sera exécutée, mais ce que l'on sait, c'est que l'objet créé sera déjà dans le DOM. if (typeof callback === "function") { // On exécute la Callback. // On a arbitrairement décidé, parce que ça semblait utile, de passer en paramètre de Callback l'objet créé. // Le développeur pourra même manipuler l'objet créé sans le re-cibler après traitement. callback(p); } }, 0); }); } })(jQuery);
Et je fais un utilisateur/développeur heureux qui cible l'id fifth-example :
// Définition du code qui sera exécuté après "addItalicTextAfter". function whatIwantExecuteAfter(element) { // "element" contient l'objet créé par "addItalicTextAfter". element .text("J'écris ce que je veux avec le style que je veux !") // On change le texte nous-mêmes au lieu de passer par la fonction "addItalicTextAfter". .addClass("like-a") // On change la class nous-mêmes au lieu de passer par la fonction "addItalicTextAfter". // On ajoute un évènement au clique sur l'élément. .click(function () { alert("« Merci monsieur Morpheus ! » - Keanu R."); }); } $("#fifth-example").addItalicTextAfter(whatIwantExecuteAfter);
Il est également possible de directement passer par une fonction anonyme. Les développeurs jQuery y sont habitués !
$("#sixth-example").addItalicTextAfter(function (element) { element .text("J'écris encore ce que je veux, et ouais !") .addClass("like-a") .click(function () { alert("« Monde de merde ! » - Laurence F."); }); });
Déléguer à la Callback est une bonne chose
On s'aperçoit aisément qu'au final, l'ajout de style, ou de texte, etc... peut également être fait par la Callback à condition de lui en donner les moyens. Il faut donc se limiter à ce que votre développement doit faire et toujours laisser des portes de sorties dans vos traitements.
Plusieurs Callbacks pour un traitement
Il ne faut pas hésiter à parsemer son code de Callbacks aux endroits stratégiques pour changer le comportement de votre code. Dans notre exemple précédent, on pourrait avoir une Callback pour chaque élément traité et une Callback quand tous les éléments sont traités.
Les mauvaises pratiques pour rattraper la boulette
Comme toujours les solutions ne manquent pas quand un développeur sympa à « oublié » de proposer une Fonction de rappel dans son développement qu'il a eu la délicatesse de minifier.
La vilaine fonction jQuery live
La fonction jQuery(target).live(event, callback) est utilisé à la place de jQuery(target).bind(event, callback) (ou jQuery(target).on(event, callback)) par les développeurs qui ont une méconnaissance totale de l'ordre d'exécution d'un code dans plusieurs fichiers JavaScript éparpillés sur une page HTML elle-même remplit de balises <script> dans tous les coins...
Mais sinon elle permet d'associer un évènement sur un élément du DOM qui n'est pas encore présent dans celui-ci. Nous allons l'utiliser de la manière la « plus propre » possible pour corriger le problème précédent. Écoutons en « live » l'arrivée d'éléments après l'id seventh-example.
$("#seventh-example").addItalicTextAfter({ class: "my-target like-a" }); // Je décide d'écouter en boucle les nouveaux éléments ".my-target" ajouté dans le DOM. // Cette action se fera à intervalle régulier et le code exécuté ci-dessous l'ai lui-même de manière asynchrone. $(".my-target").live("click", function () { // Dès qu'un élément ".my-target" est ajouté au DOM l'évènement "onclick" lui est ajouté. alert("« Hey, Mais ça sent la merde ? » - Christophe L."); // Nous allons arrêter cette écoute de nouveaux éléments ajoutés car nous savons qu'il n'y en aura pas d'autres. $(this).die("click"); // Ça, "die", c'est ce qui rend "assez propre" l'utilisation de live. });
Note : la fonction die n'enlève pas l'évènement "onlclick" d'un élément. Elle se contente de faire arrêter l'écoute des nouveaux éléments ciblés dans le DOM qui sont arrivés.
Vous pouvez cependant constater que cela a marché car il n'y a eu qu'un seul élément d'ajouté. S'il y en avait eu plusieurs, il aurait fallu exécuter die qu'avec le dernier arrivé ce qui aurait compliquer la tâche. Une simple fonction de Callback, c'est tout ce qu'il vous aurait fallu.
L'inconvénient de live c'est qu'il ne marche qu'avec un nombre limité d'évènement. Voyons juste après la méthode universelle.
Le vilain setInterval passe partout
Avec, comme précédemment, une utilisation propre du setInterval on peut également s'en sortir. Voyez plutôt ça sur l'id eighth-example.
$("#eighth-example").addItalicTextAfter({ class: "my-example like-a" }); // On va vérifier à intervalle de 50 millisecondes le nombre d'éléments ".my-example" sur la page. var tempExampleInterval = setInterval(function () { // On s'attend au maximum à en recevoir un. if ($(".my-example").length == 1) { // Dès qu'on a un ".my-example" dans le DOM on ajoute l'évènement à l'objet... $(".my-example").click(function () { alert("« Nan mais c'est bon tu m'as soûlé moi, j'en veux pas de ton monde pourri moi... » - Keanu R."); }); // ...et on arrête le "setInterval". clearInterval(tempExampleInterval); } }, 50);
Comme pour l'exemple précédent, il n'y a pas de solution miracle. Il faut connaître le nombre d'élément qu'on s'attend à avoir pour mettre fin au timer. Si on ne le fait pas, notre page peut vite devenir une foire aux timers et aux codes inutilements exécutés.
Un petit cas concret avec le script SyntaxHighlighter
Le code JavaScript présenté dans cet article (à l'heure où j'ecris ces lignes) est écrit à la source sans couleurs. C'est le script JavaScript SyntaxHighlighter qui repasse sur tous les éléments pour les colorier, leur ajouter un nombre de ligne, etc... Cette transformation est asynchrone et il n'existe pas de Callback.
Utilisation d'un setInterval pour remplacer la Callback
Voici le code utiliser pour vous permettre "d'agripper" les exemples de code au clique gauche de la souris et de faire défiler le code.
var // On compte les éléments qui vont nécessiter d'être transformé. // Le développeur a pensé à cette fonctionnalité. // Mais a oublié de mettre une Callback... beforeHighlighted = SyntaxHighlighter.findElements().length, afterHighlighted; // Utilisation standard du script. // Voilà, à la fin de l'exécution de cette fonction asynchrone, notre code sera tout beau. SyntaxHighlighter.all(); // Maintenant je veux permettre de glisser le code sur l'axe des y en maintenant ma souris enfoncée. // C'est impossible car les éléments à accrocher n'existerons qu'à la fin de l'exécution de "SyntaxHighlighter.all();", fin d'exécution que je ne connais pas... // Et comme déjà dit (il me semble) : pas de Callback. // On va boucler à l'intérieur de cette propre fonction jusqu'à ce que le traitement puisse commencer. (function eachElementHighlighted() { // Nous allons vérifier au bout de 100 milisecondes... setTimeout(function () { // ...le nombre d'élément déjà transformé. afterHighlighted = $('.syntaxhighlighter'); // S'il y a autant d'élément transformé que d'élément qu'il fallait transformer on passe dans le else. // Sinon on revérifie une nouvelle fois dans le if. if (afterHighlighted.length < beforeHighlighted) { eachElementHighlighted(); } else { /***************************************************************/ /* Ici commence seulement le code utile à la fonctionnalité !! */ /***************************************************************/ if (!Modernizr.touch) { var $sh = $(".syntaxhighlighter").css("overflow","hidden"); $sh.mousedown(function (e) { $.data(this, "draggable", true); $.data(this, "offset", e.pageX); }).mouseup(function () { $.data(this, "draggable", false); $.data(this, "offset", 0); }).mouseleave(function () { $.data(this, "draggable", false); $.data(this, "offset", 0); }).mousemove(function (e) { if ($(this).data("draggable")) { $(this).scrollLeft(parseInt($(this).scrollLeft() + ($(this).data("offset") - e.pageX), 10)); $.data(this, "offset", e.pageX); } }).data("draggable", false).data("offset", 0); } /**************************************************************/ } }, 100); // On a mis 100 millisecondes. Il faut trouver le compromis entre nombre de vérification et perte de temps possible avant première vérification. })();
Adieu setInterval, merci Fonction de rappel !
Voici ce que pourrait donner le code précédent avec une Callback...
SyntaxHighlighter.all(function ($sh) { if (!Modernizr.touch) { $sh.css("overflow","hidden"); $sh.mousedown(function (e) { $.data(this, "draggable", true); $.data(this, "offset", e.pageX); }).mouseup(function () { $.data(this, "draggable", false); $.data(this, "offset", 0); }).mouseleave(function () { $.data(this, "draggable", false); $.data(this, "offset", 0); }).mousemove(function (e) { if ($(this).data("draggable")) { $(this).scrollLeft(parseInt($(this).scrollLeft() + ($(this).data("offset") - e.pageX), 10)); $.data(this, "offset", e.pageX); } }).data("draggable", false).data("offset", 0); } });
Ah ouais quand même...
1 Commentaire
Choisir un Avatar depuis une url
Adresse : (64px x 64px)ou Changer la couleur de fond
et Choisir un Avatar dans la liste
Votre commentaire a été ajouter ci-dessous ! Si vous désirez le supprimer ultérieurement, servez vous du code suivant :
Les commentaires sont actuellement affiché du plus rescent au plus ancien. Vous pouvez inverser l'ordre en cliquant ci-dessous :
Tu es un dingue Bruno (c'est un compliment). ;-)
Les développeurs front-end tel que toi cela ne court pas les rues !
Bonne continuation,
Pierre
Je trouve ce commentaire pertinent ?
Je trouve ce commentaire pertinent ?