Exercices en jQuery
Mes notes sur jQuery
6 - Gestion des événements
18 février 2014

Ici, je décris comment gérer les événements jQuery au moyen de scripts appelés gestionnaires d'événements, réactions ou, en anglais, event handlers ou callback. Il existe de nombreuses méthodes pour associer événements et sélections et j'ai essayé de les examiner toutes avec attention.

Tous les événements sont détectés per le fureteur lorsqu'ils surviennent et le fureteur exécute alors l'action par défaut comme suivre un lien hypertexte lorsque l'utilisateur a cliqué dessus. Si l'élément cliqué n'a pas de fonction particulière, rien n'arrive suite à l'événement.

Pourtant, l'idée maitresse de l'existance des événements est de pouvoir faire quelque chose lorsqu'ils surviennent. C'est exactement la raison pour laquel on peut écrire un "script" pour gérer l'événement mais il faut aussi l'enregistrer en tant que gestionnaire d'événement (event handler ou callback) si on veut que jQuery l'exécute lorsque l'événement survient.

Pour ce faire, on peut placer un événement dans le code source d'une page comme suit:

<div onClick="action();"></div>

dans lequel cas, un clic sur cette <div> provoquera l'appel de la fonction action(). Avec jQuery on procedera autrement :

$("div").click(function(){
  alert("Quelqu'un a cliqué cette div");
});

où la fonction anonyme qui apparaît comme argument de .click() est appelé "callback" ou "event handler" en anglais et gestionnaire d'événement en français. Dans le cas présent, chacune des balises <div> de la page affichera l'alerte "Quelqu'un a cliqué sur cette div" lors d'un clic.

Ça sauve beaucoup de travail. Dans le code jQuery, on enregistre un gestionnaire d'événement sur tous les éléments <div> du document en une seule ligne de code alors qu'avec le code "in-line" précédent, il faudrait enregistrer le gestionnaire pour chaque élément <div> du document pour arrriver au même résultat. Facile!

Enregistrement simple d'un gestionnaire d'événement: .bind()

Lorsqu'un évènement se produit, un objet événement est créé dynamiquement, et passé séquentiellement aux écouteurs d'événements (event listeners) qui sont autorisés à gérer l'événement. Cette association entre l'événement et son écouteur d'événement s'appelle "enregistrer" un gestionnaire d'événement (event handler ou callback): on fournit le code à exécuter et on dit à jQuery quels éléments du DOM vont exécuter cette réaction. Les gestionnaires d'évènements peuvent ainsi être attachés à différents éléments dans le DOM.

Comme on l'a vu précédemment, la façon la plus simple de le faire est la suivante:

$("div").click(function(){
  alert("Quelqu'un a cliqué cette div");
});

qui est un raccourci pour

$("div").bind("click", function(){
  alert("Quelqu'un a cliqué cette div");
});

Ce code indique à jQuery qu'un clic sur n'importe quelle <div> du document va lancer l'exécution du bloc de code inclus entre les accolades. Pour y arriver, jQuery va greffer (attach) le code du gestionnaire à chacune des balises <div> correspondant à la sélection et en faire des "écouteurs d'événements" (event listeners).

Ça peut devenir très lourd en ressources de mémoire lorsque le nombre d'éléments sélectionnés est élevé car jQuery doit alors greffer le code du gestionnaire à chacun des éléments visés.

En fait, si, après le chargement du DOM, on a créé un nouvel élément <div>, un clic sur ce dernier ne provoquera aucune réaction. L'élément se trouve dans le DOM mais jQuery ne le sait pas et ne le trouvera pas.

Ce qu'on a vu jusqu'ici est simple et ça suffit à l'usage du débutant mais c'est insuffisant pour le programmeur un peu expérimenté qui veut créer à la volée des éléments dans le DOM après son chargement. Si on n'a pas l'intention de créer ou de supprimer des éléments du DOM, on peut s'arrêter ici.

Notons en passant
que .bind()
est maintenant déprécié depuis jQuery 1.7 et on ne recommande son usage que si on sait qu'on n'ajoutera pas d'éléments à la volée et s'il est utilisé avec modération.

Comment opèrent les événements

Avant d'aller plus loin dans ce sujet, je me dois de faire une digression pour parler de la façon dont Javascript et jQuery traitent les événements et leurs gestionnaires.

Lorsqu'une page HTML se charge, elle génère l'événement .ready() au moment où le DOM est chargé et c'est à cet instant que Javascript prend un"snapshot" du DOM. Il n'aura ensuite aucune façon de mettre cet instantané à jour.

Comportement des fureteurs

Capture et délégation d'événement
[The Bubbling Technique]

En général, le modèle d'événement du DOM est basé sur deux phases principales: la capture et la délégation (bubbling) comme on le voit sur le diagramme à droite).

Lorsqu'un événement survient (ici, #text a été cliqué), il est expédié vers le bas de l'arborescence du document jusque vers l'élément où l'événement est survenu, la cible, c'est la capture. Il est ensuite retourné vers le haut de l'arborescence à partir de l'élément cliqué où il peut être capté par n'importe quel élément parent de la hiérarchie qui va alors gérer l'événement, c'est la délégation.

C'est ici qu'on voit pourquoi Internet Explorer fait bande à part chez les fureteurs. Au début, il y avait Netscape et Microsoft. Ils ont choisis des modèles d'événements différents. Internet Explorer utilise la capture alors que tous les autres fureteurs, conformes aux normes du W3C où le modèle d'événement, utilise un mélange capture/délégation. Ce sont des différences au niveau des moteurs Javascript des fureteurs que les problèmes d'incompatibilité surviennent. C'était une plaie pour la programmation Javascript. C'est devenu transparent et facile avec jQuery.

Pour plus de détail,
je recommande la série d'articles écrite par Peter-Paul Koch intitulés "Events"

Quoi faire si de nouveaux éléments du DOM sont créés à la volée

Jusqu'à l'arrivée de la version 1.7 de jQuery, on utilisait les méthodes live() et delegate() pour greffer des événements à des éléments du DOM sélectionnés par $().

La méthode live()

C'est ici que .live(), introduit avec la version 1.3 de jQuery, sauve la mise. Si on veut que le gestionnaire affecte non seulement le comportement des éléments chargés avec le DOM mais aussi ceux qui vont être créés à la volée après, il suffit de remplacer .bind() par .live() comme suit:

$("div").live(function(e) {
  alert("Quelqu'un a cliqué cette div");
});

En effectuant des recherches sur le Web, j'ai trouvé une définition intéressante de la méthode live():

« Attache une fonction à un événement pour tous les éléments qui répondent au sélecteur, immédiatement et dans le futur. » [jQuery : live vs delegate]

Dans ce scénario, le gestionnaire d'événement n'est pas greffé à l'élément cliqué mais bien à l'élément "document" (en haut de la hiérarchie). Lorsque l'élément nouvellement créé est cliqué, il n'a pas de gestionnaire greffé mais le processus de délégation se met alors en route vers le haut de la hiérarchie et trouve son gestionnaire au niveau de l'élément "document". Il est alors exécuté.

Ceci sera fonctionnel même avec des éléments html ajouté dynamiquement n'importe où dans la racine du document. De cette façon, on s'assure que le gestionnaire s'appliquera à tous les éléments sélectionnés par le sélecteur, qu'ils soient dans le DOM lors de son chargement ou qu'ils soient créés à la volée par la suite.

Avant la version 1.7,
il était très courant (et ça l'est encore ajourd'hui) d'utiliser la méthode live(), pour déléguer un évènement à un élément créé dynamiquement. C'est une technique dépréciée, pour la simple et bonne raison qu'elle devient lente sur de grosses pages.

La méthode delegate()

La méthode delegate() fait mieux encore.Considérons l'exemple suivant:

$("#container").delegate("div", "click", function(e){
  alert("Quelqu'un a cliqué cette div");
});

Dans ce scénario, le gestionnaire d'événement n'est pas greffé à l'élément cliqué mais bien à l'élément "#container" (plus haut de la hiérarchie que l'événement cliqué mais en bas de la racine du document). Lorsque l'élément nouvellement créé est cliqué, il n'a pas de gestionnaire greffé mais le processus de délégation se met alors en route vers le haut de la hiérarchie et trouve son gestionnaire au niveau de l'élément "#container". Il est alors exécuté.

Introduite avec la version 1.4.2 de jQuery, elle est aissi définie comme comme suit:

« Attache une fonction à un ou plusieurs événements pour tous les éléments qui répondent au sélecteur, immédiatement et dans le futur basé sur les spécification de l’élément racine. » [ jQuery : live vs delegate]

citation à laquelle j'ai mis en gras et en italique ce qui était important. Avec delegate(), le gestionnaire d'événement n'est pas greffé à l'élément racine du document, mais bien au sélecteur (tous les éléments <div> de "#container" vont réagir à l'événement). Pour obtenir le même réssultat avec live(), il faudrait écrire

$("div", "#container").live(function(e) {
  alert("Quelqu'un a cliqué cette div");
});

en prenant "#container" comme contexte du sélecteur.

Un enregistrement plus complexe mais plus puissant

Voilà la fonction qui change tout. Très fortement encouragée dans toutes les docs depuis la version 1.7, la fonction .on() combine tous les avantages de .bind(), .live() et delegate(). Et elle y parvient en effet très bien. Avec deux arguments, elle remplace .bind() alors qu'avec trois arguments, elle fait de même pour delegate().

Avec on(), on remplace bind() par on() donc jusque là ça va :

$("div").on('click', function() {
  alert("Quelqu'un a cliqué cette div");
});

Ainsi, tous les éléments correspondants aux critères du sélecteur sont dotés d'un gestionnaire d'événement.

Ce qui est intéressant, c’est que .on() gère également le « comportement » de .live() et de manière intelligente. .on() avec 3 arguments est le nouveau .live(). Attention, le sélecteur sera désormais le deuxième argument. En revanche, l’élément ciblé par le .on() devra être (pour davantage de performance) le sélecteur parent le plus proche de l'élément ciblé (dans cet exemple, ce sera #container).

$("#container").on("click", "div", function() {
  alert("Quelqu'un a cliqué cette div");
});​

L'avantage de cette façon de faire est que seul le parent (#container) sera doté d'un gestionnaire d'événement qui captera l'événement suite au phénomène de délégation et activera son gestionnaire d'événement. Ce phénomène décrit ce qui arrive lorsqu'un événement est déclanché sur un élément du DOM: il se propage alors en montant la chaîne de tous ses ancêtres jusqu'à l'élément document. Comme #container se trouve dans cette chaîne et qu'il dispose d'un gestionnaire d'événement, il va le gérer.

Le fait qu'un ou plusieurs éléments sujet à cette action ait été créé après le chargement du DOM n'a alors aucune importance puisque "#container", lui, existait et a été doté du gestionnaire.

À ce stage, il est important de noter que, suite à l'utilisation de .on(), $(this) ne sera pas "#container" mais bien l'élément <div> qui aura été cliqué (la propriété "target" de l'objet événement le contient).

Pour une page sur laquelle on ne change pas le contenu, on ne voit pas de différence entre l'utilisation de .bind() ou de .on(). Si on ajoute des éléments dynamiquement, il faut utiliser .on().

Lés événement délégués ont l'avantage de pouvoir gérer les événements de leurs éléments descendants qui sont ajoutés au document après l'enregistrement de l'événement. Avec les événements délégués, on peut ainsi éviter d'avoir à ajouter ou à supprimer des gestionnaires d'événement: les événements sont gérés par l'élément parent désigné dans le sélecteur.

Une des difficultés qu'on rencontrait avec Javascript avait à voir avec la gestion des énénements qui variait d'un fureteur à l'autre (surtout avec Internet Explorer). JQuery a éliminé cette difficulté en définissant une application d'interface programmable uniforme qui donne les résultats attendus avec tous les fureteurs.

Annexes

A - Un peu plus sur la propagation des événements

Illustration de la capture et de la délégation [tiré de W3C]

Comme on l'a vu dans ce qui précède, tous les fureteurs qui se rallient aux normes du W3C utilisent un processus mixte de capture et de délégation dans leur recherche des gestionnaires d'événement alors que Internet Explorer utilise la capture. C'est une source d'incompatibilité.

Je ne vais pas m'aventurer beaucoup plus loin sur le sujet mais j'ai trouvé un article de Peter-Paul Koch où il pose la question suivante:

« If an element and one of its ancestors have an event handler for the same event, which one should fire first? » [Event order]

Sa réponse a été courte: « Ça dépend du fureteur ». Les vieilles versions d'Internet Explorer utilisaient la capture, de sorte que l'événement parent était exécuté avant l'événement enfant alors que les autres fureteurs offraient le choix, un choix qui s'effectuait en utilisant la fonction addEventListener(). Cette fonction est utilisée pour enregistrer un gestionnaire d'événement et elle est définie comme

element.addEventListener(Type, Listener, useCapture);

Type représente le type d'événement, Listener est l'objet qui recevra une notification lorsque le type d'événement se produit et useCapture est une booléen qui, lorsque "vrai" utilise la capture et, lorsque "faux", utilise la délégation.

Avant sa version 9, Internet Explorer utilisait la fonction addEvent() pour attacher un gestionnaire d'événement. Depuis la version 9, il utilise addEventListener().

En regardant un peu le code de jQuery, je me suis aperçu que jQuery utilise toujours addEventListener() avec la valeur de useCapture à "faux": il utilise donc la délégation.


Pour toute question ou commentaire
E-Mail
page modifiée le 15 avril 2014 à 19:02:40. []