mitmario.dev

Bubbling

JavaScript Im Browser 2 Min Lesezeit 3 BeispieleLektion 5 von 9

Ein Klick auf einen Knopf ist nicht nur ein Klick auf den Knopf. Er ist auch einer auf das Element darum, und auf das darum, bis hinauf zum document.

Das Ereignis steigt auf

Ein Klick, drei Listener
const protokoll = document.getElementById("protokoll");

function notiere(text) {
  protokoll.textContent = `${protokoll.textContent}${text}\n`;
}

// Drei Listener auf drei Ebenen, alle auf "click".
document.getElementById("innen").addEventListener("click", (event) => {
  notiere(`innen (target war ${event.target.tagName})`);
});

document.getElementById("mitte").addEventListener("click", (event) => {
  notiere(`mitte (target war ${event.target.tagName})`);
});

document.getElementById("aussen").addEventListener("click", (event) => {
  notiere(`außen (target war ${event.target.tagName})`);
});

console.log("Klick auf den Knopf: drei Zeilen. Klick daneben: weniger.");

Klickst du im ersten Beispiel den Knopf, läuft dasselbe Ereignis nacheinander durch alle drei Listener: erst innen, dann mitte, dann außen. Von innen nach außen, deshalb der Name Bubbling: es steigt auf wie eine Blase.

Dabei ist es wirklich dasselbe Ereignis. event.target bleibt auf allen drei Ebenen der Knopf, denn dort ist es passiert. Was sich ändert, ist event.currentTarget: das ist jeweils das Element, dessen Listener gerade läuft. Genau der Unterschied aus Lektion 12.2, und hier sieht man, wozu er da ist.

Klickst du daneben statt auf den Knopf, laufen entsprechend weniger Listener. Der Weg beginnt eben immer dort, wo der Klick war.

Drei Phasen

Die drei Phasen und das dritte Argument
const protokoll = document.getElementById("protokoll");

function notiere(text) {
  protokoll.textContent = `${protokoll.textContent}${text}\n`;
}

const aussen = document.getElementById("aussen");

// Mit true als drittem Argument: Capturing. Läuft auf dem
// Weg nach UNTEN, also vor dem Ziel.
aussen.addEventListener("click", () => notiere("1. außen, Capturing"), true);

// Am Ziel selbst.
document.getElementById("innen").addEventListener("click", () => notiere("2. innen, Ziel"));

// Ohne drittes Argument: Bubbling. Läuft auf dem Weg nach OBEN.
aussen.addEventListener("click", () => notiere("3. außen, Bubbling"));

console.log("Ein einziger Klick, drei Zeilen, feste Reihenfolge.");

Genau genommen läuft ein Ereignis zweimal durch den Baum.

Capturing: von ganz oben nach unten bis zum Ziel. Ziel: am Element selbst. Bubbling: vom Ziel wieder nach oben.

addEventListener hängt den Listener standardmäßig an die Bubbling-Phase. Ein true als drittes Argument hängt ihn stattdessen an die Capturing-Phase, und dann läuft er vor dem Ziel.

Im Alltag braucht man das fast nie. Es lohnt sich trotzdem, es einmal gesehen zu haben, denn es erklärt, warum ein Listener manchmal früher feuert, als man erwartet.

stopPropagation

stopPropagation, und was es kostet
const protokoll = document.getElementById("protokoll");

function notiere(text) {
  protokoll.textContent = `${protokoll.textContent}${text}\n`;
}

document.getElementById("frei").addEventListener("click", () => {
  notiere("frei: geklickt");
});

document.getElementById("gestoppt").addEventListener("click", (event) => {
  notiere("gestoppt: geklickt");
  event.stopPropagation();
});

// Stell dir vor, das hier hat jemand anders geschrieben:
// ein Menü, das sich schließen soll, sobald man irgendwo
// in den Bereich klickt. Beim zweiten Knopf erfährt es
// davon nichts mehr.
document.getElementById("aussen").addEventListener("click", () => {
  notiere("außen: Menü würde jetzt schließen");
});

console.log("Klick beide Knöpfe und vergleich die Protokolle.");

event.stopPropagation() beendet die Reise. Alle Listener weiter oben bekommen das Ereignis nicht mehr.

Das ist gelegentlich genau richtig, und es hat einen Preis, den man selten sofort sieht: du schneidest Handler ab, von denen du nichts weißt.

Im dritten Beispiel steht außen ein Listener, der ein Menü schließen soll. Beim zweiten Knopf erfährt er nichts mehr davon, und das Menü bleibt offen. Fehler dieser Art sind unangenehm zu finden, weil die Ursache und die Wirkung an ganz verschiedenen Stellen im Code stehen.

Die ehrliche Empfehlung: Nimm stopPropagation sparsam und nur, wenn du weißt, wem du damit etwas wegnimmst. In den allermeisten Fällen, in denen es eingesetzt wird, wäre eine genauere Prüfung im äußeren Handler die bessere Lösung gewesen.

Es gibt außerdem stopImmediatePropagation(), das zusätzlich die übrigen Listener am selben Element abschneidet. Dafür gibt es noch weniger gute Gründe.

Was nicht aufsteigt

Vier Ereignisse steigen nicht auf, und alle vier hast du in Lektion 12.3 schon gesehen: focus, blur, mouseenter und mouseleave.

Für die ersten beiden gibt es Ersatz, der aufsteigt: focusin und focusout. Für die anderen beiden mouseover und mouseout.

Das ist keine Kleinigkeit, sondern eine Einschränkung für die nächste Lektion: Was nicht aufsteigt, lässt sich nicht delegieren.

Zum Mitnehmen

Ein Ereignis passiert unten und läuft nach oben. Das ist kein Nebeneffekt, sondern die Grundlage der Event-Delegation aus der nächsten Lektion.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.