Das Denken hinter der Vereinfachung von Event- Handlern

Avatar of Tiger Oakes
Tiger Oakes am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Events werden verwendet, um zu reagieren, wenn ein Benutzer irgendwo klickt, mit der Tastatur auf einen Link fokussiert oder den Text in einem Formular ändert. Als ich begann, JavaScript zu lernen, schrieb ich komplizierte Event-Listener. In jüngerer Zeit habe ich gelernt, sowohl die Menge des Codes, den ich schreibe, als auch die Anzahl der benötigten Listener zu reduzieren.

Beginnen wir mit einem einfachen Beispiel: ein paar ziehbare Boxen. Wir möchten dem Benutzer anzeigen, welche farbige Box er gezogen hat.

<section>
  <div id="red" draggable="true">
    <span>R</span>
  </div>
  <div id="yellow" draggable="true">
    <span>Y</span>
  </div>
  <div id="green" draggable="true">
    <span>G</span>
  </div>
</section>

<p id="dragged">Drag a box</p>

Siehe den Pen
Dragstart-Events
von Tiger Oakes (@NotWoods)
auf CodePen.

Der intuitive Weg, es zu tun

Als ich anfing, mit JavaScript-Events zu arbeiten, schrieb ich für jedes Element separate Event-Listener-Funktionen. Das ist ein gängiges Muster, weil es der einfachste Einstieg ist. Wir wollen spezifisches Verhalten für jedes Element, also können wir spezifischen Code dafür verwenden.

document.querySelector('#red').addEventListener('dragstart', evt => {
  document.querySelector('#dragged').textContent = 'Dragged red';
});

document.querySelector('#yellow').addEventListener('dragstart', evt => {
  document.querySelector('#dragged').textContent = 'Dragged yellow';
});

document.querySelector('#green').addEventListener('dragstart', evt => {
  document.querySelector('#dragged').textContent = 'Dragged green';
});

Reduzierung von doppeltem Code

Die Event-Listener in diesem Beispiel sind alle sehr ähnlich: Jede Funktion zeigt einen Text an. Dieser doppelte Code kann zu einer Hilfsfunktion zusammengefasst werden.

function preview(color) {
  document.querySelector('#dragged').textContent = `Dragged ${color}`;
}

document
  .querySelector('#red')
  .addEventListener('dragstart', evt => preview('red'));
document
  .querySelector('#yellow')
  .addEventListener('dragstart', evt => preview('yellow'));
document
  .querySelector('#green')
  .addEventListener('dragstart', evt => preview('green'));

Das ist viel übersichtlicher, aber es erfordert immer noch mehrere Funktionen und Event-Listener.

Nutzung des Event-Objekts

Das Event-Objekt ist der Schlüssel zur Vereinfachung von Listenern. Wenn ein Event-Listener aufgerufen wird, sendet er auch ein Event-Objekt als erstes Argument. Dieses Objekt enthält Daten, die das aufgetretene Ereignis beschreiben, wie z. B. den Zeitpunkt des Ereignisses. Um unseren Code zu vereinfachen, können wir die evt.currentTarget-Eigenschaft verwenden, wobei currentTarget sich auf das Element bezieht, an das der Event-Listener angehängt ist. In unserem Beispiel wäre das eine der drei farbigen Boxen.

const preview = evt => {
  const color = evt.currentTarget.id;
  document.querySelector('#dragged').textContent = `Dragged ${color}`;
};

document.querySelector('#red').addEventListener('dragstart', preview);
document.querySelector('#yellow').addEventListener('dragstart', preview);
document.querySelector('#green').addEventListener('dragstart', preview);

Nun gibt es nur noch eine Funktion anstelle von vier. Wir können dieselbe Funktion als Event-Listener wiederverwenden und evt.currentTarget.id hat je nach auslösendem Element einen anderen Wert.

Nutzung von Bubbling

Eine letzte Änderung besteht darin, die Anzahl der Zeilen in unserem Code zu reduzieren. Anstatt jedem Kasten einen Event-Listener zuzuweisen, können wir einen einzigen Event-Listener an das <section>-Element anhängen, das alle farbigen Boxen enthält.

Ein Ereignis beginnt im Element, in dem das Ereignis ausgelöst wurde (einer der Kästen), wenn es ausgelöst wird. Dort wird es jedoch nicht stoppen. Der Browser durchläuft jedes Elternelement dieses Elements und ruft alle darauf angehängten Event-Listener auf. Dies wird fortgesetzt, bis die **Wurzel** des Dokuments erreicht ist (das <body>-Tag in HTML). Dieser Prozess wird als „Bubbling“ bezeichnet, da das Ereignis wie eine Blase durch den Dokumentenbaum aufsteigt.

Durch das Anhängen eines Event-Listeners an die Sektion wird das Fokus-Ereignis von der gezogenen farbigen Box zum übergeordneten Element „geblubbert“. Wir können auch die evt.target-Eigenschaft nutzen, die das Element enthält, das das Ereignis ausgelöst hat (eine der Boxen), anstatt des Elements, an das der Event-Listener angehängt ist (das <section>-Element).

const preview = evt => {
  const color = evt.target.id;
  document.querySelector('#dragged').textContent = `Dragged ${color}`;
};

document.querySelector('section').addEventListener('dragstart', preview);

Jetzt haben wir viele Event-Listener auf nur einen reduziert! Bei komplizierterem Code wird der Effekt größer sein. Durch die Nutzung des Event-Objekts und des Bubbling können wir JavaScript-Events zähmen und den Code für Event-Handler vereinfachen.

Was ist mit Klick-Events?

evt.target funktioniert gut mit Events wie dragstart und change, bei denen nur eine kleine Anzahl von Elementen einen Fokus erhalten oder bei denen eine Eingabe geändert werden kann.

Allerdings hören wir normalerweise auf click-Events, um auf einen Benutzer reagieren zu können, der auf eine Schaltfläche in einer Anwendung klickt. click-Events werden für **jedes** Element im Dokument ausgelöst, von großen divs bis zu kleinen spans.

Nehmen wir unsere ziehbaren Farbkästen und machen sie stattdessen klickbar.

<section>
  <div id="red" draggable="true">
    <span>R</span>
  </div>
  <div id="yellow" draggable="true">
    <span>Y</span>
  </div>
  <div id="green" draggable="true">
    <span>G</span>
  </div>
</section>

<p id="clicked">Clicked a box</p>
const preview = evt => {
  const color = evt.target.id;
  document.querySelector('#clicked').textContent = `Clicked ${color}`;
};

document.querySelector('section').addEventListener('click', preview);

Siehe den Pen
Klick-Events: Funktioniert noch nicht ganz
von Tiger Oakes (@NotWoods)
auf CodePen.

Wenn Sie diesen Code testen, stellen Sie fest, dass manchmal nichts an „Clicked“ angehängt wird, anstatt wenn auf eine Box geklickt wird. Der Grund, warum es nicht funktioniert, ist, dass jede Box ein <span>-Element enthält, das anstelle des ziehbaren <div>-Elements angeklickt werden kann. Da die Spans keine festgelegte ID haben, ist die Eigenschaft evt.target.id ein leerer String.

Wir interessieren uns in unserem Code nur für die farbigen Boxen. Wenn wir irgendwo innerhalb einer Box klicken, müssen wir das übergeordnete Box-Element finden. Wir können element.closest() verwenden, um das dem angeklickten Element am nächsten gelegene übergeordnete Element zu finden.

const preview = evt => {
  const element = evt.target.closest('div[draggable]');
  if (element != null) {
    const color = element.id;
    document.querySelector('#clicked').textContent = `Clicked ${color}`;
  }
};

Siehe den Pen
Klick-Events: .closest verwenden
von Tiger Oakes (@NotWoods)
auf CodePen.

Jetzt können wir einen einzigen Listener für click-Events verwenden! Wenn element.closest() null zurückgibt, bedeutet dies, dass der Benutzer irgendwo außerhalb einer farbigen Box geklickt hat und wir das Ereignis ignorieren sollten.

Weitere Beispiele

Hier sind einige zusätzliche Beispiele, die zeigen, wie man einen einzelnen Event-Listener nutzen kann.

Listen

Ein gängiges Muster ist eine Liste von Elementen, mit denen interagiert werden kann, wobei neue Elemente dynamisch mit JavaScript eingefügt werden. Wenn wir Event-Listener an jedes Element anhängen, muss Ihr Code jedes Mal, wenn ein neues Element generiert wird, Event-Listener verarbeiten.

<div id="buttons-container"></div>
<button id="add">Add new button</button>
let buttonCounter = 0;
document.querySelector('#add').addEventListener('click', evt => {
  const newButton = document.createElement('button');
  newButton.textContent = buttonCounter;
  
  // Make a new event listener every time "Add new button" is clicked
  newButton.addEventListener('click', evt => {

    // When clicked, log the clicked button's number.
    document.querySelector('#clicked').textContent = `Clicked button #${newButton.textContent}`;
  });

  buttonCounter++;

  const container = document.querySelector('#buttons-container');
  container.appendChild(newButton);
});

Siehe den Pen
Listen: kein Bubbling
von Tiger Oakes (@NotWoods)
auf CodePen.

Durch die Nutzung von Bubbling können wir einen einzigen Event-Listener am Container haben. Wenn wir viele Elemente in der App erstellen, reduziert dies die Anzahl der Listener von n auf zwei.

let buttonCounter = 0;
const container = document.querySelector('#buttons-container');
document.querySelector('#add').addEventListener('click', evt => {
  const newButton = document.createElement('button');
  newButton.dataset.number = buttonCounter;
  buttonCounter++;

  container.appendChild(newButton);
});
container.addEventListener('click', evt => {
  const clickedButton = evt.target.closest('button');
  if (clickedButton != null) {
    // When clicked, log the clicked button's number.
    document.querySelector('#clicked').textContent = `Clicked button #${clickedButton.dataset.number}`;
  }
});

Formulare

Vielleicht gibt es ein Formular mit vielen Eingaben, und wir möchten alle Benutzereingaben in einem einzigen Objekt sammeln.

<form>
  <label>Name: <input name="name" type="text"/></label>
  <label>Email: <input name="email" type="email"/></label>
  <label>Password: <input name="password" type="password"/></label>
</form>
<p id="preview"></p>
let responses = {
  name: '',
  email: '',
  password: ''
};

document
  .querySelector('input[name="name"]')
  .addEventListener('change', evt => {
    const inputElement = document.querySelector('input[name="name"]');
    responses.name = inputElement.value;
    document.querySelector('#preview').textContent = JSON.stringify(responses);
  });
document
  .querySelector('input[name="email"]')
  .addEventListener('change', evt => {
    const inputElement = document.querySelector('input[name="email"]');
    responses.email = inputElement.value;
    document.querySelector('#preview').textContent = JSON.stringify(responses);
  });
document
  .querySelector('input[name="password"]')
  .addEventListener('change', evt => {
    const inputElement = document.querySelector('input[name="password"]');
    responses.password = inputElement.value;
    document.querySelector('#preview').textContent = JSON.stringify(responses);
  });

Siehe den Pen
Formulare: kein Bubbling
von Tiger Oakes (@NotWoods)
auf CodePen.

Wechseln wir stattdessen zu einem einzigen Listener für das übergeordnete <form>-Element.

let responses = {
  name: '',
  email: '',
  password: ''
};

document.querySelector('form').addEventListener('change', evt => {
  responses[evt.target.name] = evt.target.value;
  document.querySelector('#preview').textContent = JSON.stringify(responses);
});

Fazit

Jetzt wissen wir, wie wir Event Bubbling und das Event-Objekt nutzen können, um komplexe Wirrungen von Event-Handlern zu vereinfachen und sie auf nur wenige ... und manchmal sogar auf nur einen ... zu reduzieren! Hoffentlich hat dieser Artikel Ihnen geholfen, Ihre Event-Handler in einem neuen Licht zu sehen. Ich weiß, dass dies für mich eine Offenbarung war, nachdem ich meine frühen Entwicklungsjahre damit verbracht hatte, duplizierten Code zu schreiben, um dasselbe zu erreichen.