Un clic sur un lien situé dans une carte, elle-même dans une liste, elle-même dans la page : l'événement ne concerne pas seulement le lien. Il traverse toute la lignée.
Comprendre ce trajet explique la plupart des comportements surprenants d'une interface, du menu qui se referme trop tôt au formulaire envoyé deux fois.
Définition
Cliquez dans une boîte pour la viser, puis cochez la capture sans rien changer d'autre.
event.target vaut a sur chaque ligne appelée. Seul event.currentTarget change : il vaut l'élément de la ligne.
3 écouteurs appelés sur 4. Après #liste la course s'arrête, et l'événement n'atteint jamais page.
Le journal est calculé, pas capté sur de vrais écouteurs : il applique les règles du DOM à quatre éléments imbriqués. La case pose la capture sur tous les ancêtres à la fois, là où du vrai code la choisit écouteur par écouteur. Chaque boîte ne porte qu'un écouteur, ce qui laisse de côté stopImmediatePropagation, dont la différence n'apparaît que lorsqu'un même élément en porte plusieurs.
Cliquez sur une boîte, puis activez la capture. L'ordre des ancêtres s'inverse, et c'est toute la différence entre les deux modes d'écoute.
La propagation est le trajet d'un événement à travers l'arbre du document. Il part de la racine, descend jusqu'à l'élément visé, puis remonte jusqu'à la racine. Chaque écouteur rencontré en chemin est appelé.
| Phase | Sens | event.eventPhase |
|---|---|---|
| Capture | De la racine vers la cible | 1 |
| Cible | Sur l'élément visé lui-même | 2 |
| Remontée | De la cible vers la racine | 3 |
Par défaut, addEventListener() enregistre pour la remontée. La capture demande l'option capture, et reste rare.
L'ordre d'appel
const liste = document.querySelector("#liste");
const lien = liste.querySelector("a");
liste.addEventListener("click", () => console.log("liste, capture"), true);
lien.addEventListener("click", () => console.log("lien"));
liste.addEventListener("click", () => console.log("liste, remontée"));
// Un clic sur le lien affiche, dans cet ordre :
// liste, capture
// lien
// liste, remontéeLa remontée explique pourquoi un écouteur posé sur un conteneur se déclenche alors que le clic a visé un enfant. Ce n'est pas un défaut, c'est le mécanisme sur lequel repose la Délégation d'événement.
target et currentTarget
Deux propriétés répondent à deux questions différentes, et les confondre est l'erreur la plus fréquente du domaine.
event.targetdésigne l'élément réellement visé, souvent le plus profond de l'arbre.event.currentTargetdésigne l'élément sur lequel l'écouteur en cours a été posé.
Dans une fonction classique, this vaut currentTarget. Dans une Fonction fléchée, non : la propriété explicite est alors la seule voie sûre.
Arrêter la course
lien.addEventListener("click", (event) => {
event.stopPropagation(); // la liste ne verra rien
console.log("traité ici");
});stopPropagation empêche l'événement d'aller plus loin dans l'arbre, mais laisse s'exécuter les autres écouteurs du même élément. stopImmediatePropagation coupe aussi ceux-là. Ni l'un ni l'autre n'annule l'action par défaut du navigateur, qui relève de preventDefault().
Arrêter la propagation par confort casse souvent du code écrit ailleurs : une fermeture de menu au clic extérieur, une mesure d'usage, un raccourci global. Réservez ce geste aux cas où il est vraiment nécessaire.
Questions fréquentes
Tous les événements remontent-ils ?
Non. focus, blur, mouseenter et mouseleave restent sur leur cible. Les deux premiers ont des jumeaux qui remontent, focusin et focusout, prévus exactement pour cet usage. La propriété event.bubbles tranche le cas au moment de l'exécution.
À quoi sert vraiment la phase de capture ?
À intervenir avant l'élément visé, donc avant tout écouteur posé plus bas. Elle sert à intercepter un geste de façon globale, par exemple pour bloquer les interactions pendant un chargement, ou pour rattraper un événement qui ne remonte pas.
Peut-on savoir dans quelle phase on se trouve ?
Oui, event.eventPhase rend 1, 2 ou 3 selon la phase en cours. C'est surtout un outil de diagnostic : dans du code de production, la logique dépend rarement de la phase, et repose plutôt sur la comparaison entre target et currentTarget.