mitmario.dev

Synthese: eine Komponente als Klasse

JavaScript Im Browser 4 Min Lesezeit 3 BeispieleLektion 6 von 7

In Lektion 12.8 hast du einen Zähler und eine Galerie gebaut, beide als Sammlung von Variablen und Handlern nebeneinander. Das funktioniert, solange es eines von jedem gibt.

Diese Lektion gibt demselben Muster einen Namen und macht es mehrfach einsetzbar.

Was eine Komponente ist

Eine Komponente, zweimal eingesetzt
class Zaehlwerk {
  #stand = 0;
  #wurzel;
  #feld;

  constructor(wurzel, name) {
    this.#wurzel = wurzel;
    this.#wurzel.innerHTML =
      '<strong class="name"></strong> <span class="stand">0</span> <button class="plus" type="button">+1</button>';
    this.#wurzel.querySelector(".name").textContent = name;
    this.#feld = this.#wurzel.querySelector(".stand");

    // Die Pfeilfunktion behält this. Ohne sie zeigt es
    // im Handler auf den Knopf (Lektion 7.3).
    this.#wurzel.querySelector(".plus").addEventListener("click", () => this.hoch());
  }

  hoch() {
    this.#stand = this.#stand + 1;
    this.#feld.textContent = String(this.#stand);
  }

  get stand() {
    return this.#stand;
  }
}

const links = new Zaehlwerk(document.getElementById("links"), "Links");
const rechts = new Zaehlwerk(document.getElementById("rechts"), "Rechts");

document.getElementById("hinweis").textContent =
  "Klick beide Knöpfe. Die Stände sind unabhängig voneinander.";

console.log("Zwei Objekte, zwei Stände:", links.stand, rechts.stand);

In reinem JavaScript ist eine Komponente eine Klasse mit vier Eigenschaften:

Sie bekommt ein Element, in dem sie leben darf.

Sie zeichnet sich dort hinein.

Sie verdrahtet ihre eigenen Ereignisse, niemand von außen muss das für sie tun.

Und sie kann sich wieder abbauen.

Mehr braucht es nicht. Was React, Vue und Svelte darüber hinaus tun, ist vor allem, das Neuzeichnen zu automatisieren. Das Grundmuster ist dieses hier.

Das Element kommt als Argument

Wenn die Komponente ihr Element selbst sucht
// Das Gegenbeispiel: die Klasse sucht sich ihr Element
// selbst, über eine feste id.
class SchlechtesZaehlwerk {
  #stand = 0;

  constructor() {
    this.wurzel = document.getElementById("zaehler");
    this.wurzel.innerHTML = '<span class="stand">0</span>';
  }

  hoch() {
    this.#stand = this.#stand + 1;
    this.wurzel.querySelector(".stand").textContent = String(this.#stand);
  }
}

const a = new SchlechtesZaehlwerk();
const b = new SchlechtesZaehlwerk();

a.hoch();
a.hoch();
b.hoch();

// Zwei Objekte, aber nur ein Element. Sie schreiben
// beide dorthin und überschreiben sich gegenseitig.
console.log("Im Element steht:", document.querySelector("#zaehler .stand").textContent);
console.log("Das zweite Feld ist leer:", document.getElementById("zweiter").innerHTML === "");
console.warn("Die zweite Instanz hat das Feld der ersten übernommen.");

Der wichtigste Punkt, und er sieht nach einer Kleinigkeit aus.

Eine Komponente, die sich ihr Element mit document.getElementById("zaehler") selbst sucht, funktioniert genau einmal. Beim zweiten Einsatz greifen beide Objekte auf dasselbe Element zu und schreiben sich gegenseitig um.

Bekommt sie das Element dagegen übergeben, kannst du sie so oft einsetzen, wie du willst. Und beim Testen gibst du ihr ein beliebiges Element, ohne dass eine bestimmte id im Dokument stehen muss.

Im zweiten Beispiel steht der Beweis in einer Zeile. Frag in der Eingabezeile der Console a.wurzel === b.wurzel, und die Antwort lautet true. Zwei Objekte, ein Element. a allein getippt zeigt dasselbe von der anderen Seite: SchlechtesZaehlwerk {wurzel: <div id="zaehler">}, und bei b steht dort dasselbe div.

Beim ersten Beispiel lässt sich diese Frage nicht stellen, und der Grund ist die nächste Überschrift: Dort ist die Wurzel ein privates Feld. links.wurzel antwortet mit undefined, und ein Vergleich zweier undefined sagt gar nichts. Was die beiden trennt, siehst du stattdessen an ihren Ständen.

Die Regel dahinter gilt weit über Komponenten hinaus: Was eine Klasse braucht, bekommt sie übergeben, statt es sich selbst zu besorgen.

Der Zustand bleibt drin

Der Stand des Zählers ist ein privates Feld aus Lektion 18.3, und das ist hier kein Selbstzweck.

Wenn jeder von außen komponente.stand = 99 schreiben könnte, stünde danach im Objekt eine Zahl und auf dem Bildschirm eine andere. Es gibt keinen Weg, das zu verhindern, außer den Zugriff gar nicht erst anzubieten.

Probier es im ersten Beispiel aus. Klick zweimal auf den linken Knopf und einmal auf den rechten, dann antwortet links.stand in der Eingabezeile der Console mit 2 und rechts.stand mit 1. Zwei Objekte, zwei Stände, und beide stimmen mit dem überein, was daneben steht.

Jetzt der Versuch von außen: links.stand = 99 antwortet mit 99, als wäre nichts gewesen. Und links.stand sagt danach weiter 2. Weil es nur einen Getter gibt und keinen Setter, läuft die Zuweisung ins Leere, ohne sich zu beschweren. Das sieht auf den ersten Blick unhöflich aus und ist die richtige Antwort: Die Zahl auf dem Bildschirm und die Zahl im Objekt können so gar nicht erst auseinanderlaufen.

Und links allein getippt antwortet mit Zaehlwerk {}. Ein Objekt, das von außen leer aussieht, obwohl es drei Felder hat. Vergleich das mit dem Kurs {titel: "JavaScript", stunden: 50} aus Lektion 18.1: Das ist der ganze Unterschied zwischen Daten, die herumliegen, und einem Zustand, der jemandem gehört.

Nach außen gibt die Komponente deshalb nur zwei Dinge: Methoden, die etwas tun, und Getter, die etwas verraten. Geändert wird der Zustand nur von innen, und wer ihn ändert, zeichnet auch neu. Damit können die beiden nicht auseinanderlaufen.

Und sie räumt hinter sich auf

Aufräumen gehört dazu
const zaehlung = document.getElementById("zaehlung");
let offeneListener = 0;

class Uhr {
  #wurzel;
  #kennung;
  #beiKlick;

  constructor(wurzel) {
    this.#wurzel = wurzel;
    this.#wurzel.innerHTML = '<span class="zeit">0</span> <button class="plus" type="button">+1</button>';

    this.#beiKlick = () => console.log("Klick");
    this.#wurzel.querySelector(".plus").addEventListener("click", this.#beiKlick);
    offeneListener = offeneListener + 1;

    // Auch ein laufender Timer gehört aufgeräumt.
    let sekunden = 0;
    this.#kennung = setInterval(() => {
      sekunden = sekunden + 1;
      const feld = this.#wurzel.querySelector(".zeit");
      if (feld) {
        feld.textContent = String(sekunden);
      }
    }, 1000);

    melden();
  }

  zerstoeren() {
    this.#wurzel.querySelector(".plus").removeEventListener("click", this.#beiKlick);
    clearInterval(this.#kennung);
    this.#wurzel.innerHTML = "";
    offeneListener = offeneListener - 1;
    melden();
  }
}

function melden() {
  zaehlung.textContent = `Offene Listener: ${offeneListener}`;
}

let uhr = null;
const platz = document.getElementById("platz");

document.getElementById("anlegen").addEventListener("click", () => {
  if (!uhr) {
    uhr = new Uhr(platz);
  }
});

document.getElementById("abbauen").addEventListener("click", () => {
  if (uhr) {
    uhr.zerstoeren();
    uhr = null;
  }
});

melden();

Das ist der Teil, den fast jeder weglässt, und Lektion 12.7 hat schon gesagt, warum das teuer wird.

Eine Komponente, die entfernt wird, hinterlässt sonst: Listener, die auf Elemente zeigen, die es nicht mehr gibt. Ein setInterval, das weiterläuft und in ein leeres Feld schreibt. Einen Beobachter, der weiter beobachtet.

Nichts davon fällt sofort auf. Es fällt nach zwanzig Minuten auf, wenn die Seite träge wird und niemand weiß, warum.

Deshalb hat jede Komponente, die etwas anmeldet, eine Methode zum Abmelden. Sie heißt hier zerstoeren, in anderen Zusammenhängen destroy, dispose oder unmount. Der Name ist egal, das Vorhandensein nicht.

Die Regel, an der du dich festhalten kannst: Für jedes addEventListener im constructor steht ein removeEventListener in der Abbaumethode, für jedes setInterval ein clearInterval. Und damit das Abmelden überhaupt möglich ist, muss der Handler in einem Feld liegen. Eine Pfeilfunktion, die direkt in addEventListener steht, lässt sich nie wieder entfernen, weil niemand mehr eine Referenz auf sie hat.

Zum Mitnehmen

Eine Komponente bekommt ihr Element, zeichnet sich hinein, verdrahtet sich selbst und kann sich wieder abbauen. Der letzte Teil ist der, den alle vergessen.

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.