Kurs JavaScript i TypeScript · Moduł 8: Zdarzenia i DOM
Dobre praktyki przy obsłudze wydarzeń
Kluczowe zasady dla efektywnego i bezpiecznego zarządzania wydarzeniami w aplikacjach JavaScript.
"Większość awarii w centrum kontroli nie bierze się z trudnych problemów," mówi Ray Arnold, przeglądając dziennik incydentów. "To drobiazgi: czujnik podpięty do urządzenia, którego nie ma, alarm, którego nikt nie wyłączył, blokada założona na wszelki wypadek." Ta lekcja zbiera takie drobiazgi w zasady, które warto mieć we krwi. Każdą poznałeś już w działaniu, teraz układamy je w jedną checklistę.
Zasady dobrego kodu
Pierwsza zasada chroni przed najczęstszym błędem początkujących. Zanim podepniesz listener, upewnij się, że element w ogóle istnieje.
// 1. Zawsze sprawdzaj, czy element istnieje
const button = document.getElementById('my-button');
if (button) {
button.addEventListener('click', handleClick);
} else {
console.warn('Przycisk o ID "my-button" nie został znaleziony');
}Metoda getElementById() zwraca null, gdy na stronie nie ma elementu o podanym ID, na przykład przez literówkę albo skrypt uruchomiony przed wczytaniem HTML. Wywołanie addEventListener() na null rzuca TypeError i zatrzymuje resztę skryptu. Skrótem byłby zapis button?.addEventListener(...), ale ja wolę jawny warunek z ostrzeżeniem, bo cichy ?. ukrywa literówkę, zamiast ją zgłosić.
Druga zasada dotyczy elementów, które pojawiają się i znikają. Zamiast podpinać listener do każdego przycisku, słuchaj na ich wspólnym rodzicu.
// 2. Używaj delegacji dla dynamicznych elementów
const parent = document.getElementById('dynamic-content');
parent.addEventListener('click', (e) => {
if (e.target.matches('.dynamic-button')) {
handleDynamicButtonClick(e);
}
});Warunek e.target.matches('.dynamic-button') przepuszcza tylko kliknięcia w przyciski z tą klasą, więc nowe przyciski działają bez żadnych dodatkowych listenerów. Pamiętaj z lekcji o delegacji, że matches() sprawdza wyłącznie kliknięty element. Jeśli w przycisku siedzi ikona, bezpieczniej użyć closest().
Trzecia zasada to sprzątanie. Komponent, który podpina listener, powinien też umieć go zdjąć.
// 3. Pamiętaj o czyszczeniu event listenerów
class ParkComponent {
constructor(element) {
this.element = element;
this.handleClick = this.handleClick.bind(this);
this.init();
}
init() {
this.element.addEventListener('click', this.handleClick);
}
handleClick(e) {
console.log('Kliknięto komponent parku');
}
destroy() {
this.element.removeEventListener('click', this.handleClick);
}
}Kluczowa jest linia z bind(this) w konstruktorze. Metoda bind() tworzy nową funkcję z na stałe przypiętym this, a my zapisujemy ją jako this.handleClick. Dzięki temu init() i destroy() używają tej samej referencji, więc usunięcie działa. Bez bind() wewnątrz handleClick() wartość this wskazywałaby element, a nie komponent.
Czwarta zasada dotyczy blokowania domyślnych zachowań przeglądarki.
// 4. Używaj preventDefault() świadomie
const form = document.getElementById('contact-form');
form.addEventListener('submit', (e) => {
e.preventDefault(); // Zatrzymuje standardowe wysłanie formularza
// Własna logika walidacji i wysyłania
if (validateForm()) {
submitFormAjax();
}
});Tutaj preventDefault() ma konkretny powód: zatrzymuje standardowe wysłanie i przeładowanie strony, bo formularz obsłuży nasz własny kod. Nie wywołuj go na wszelki wypadek. Na linku zablokuje nawigację, przy dotyku może zatrzymać przewijanie, a w listenerze pasywnym zostanie po prostu zignorowany.
Piąta zasada: rejestruj listenery jednym stylem. Właściwość onclick przechowuje tylko jedną funkcję, a addEventListener() dopisuje kolejnych słuchaczy.
// onclick przechowuje tylko jedną funkcję - druga nadpisuje pierwszą
alarmButton.onclick = () => console.log('Alarm w sektorze A');
alarmButton.onclick = () => console.log('Alarm w sektorze B');
// addEventListener dopisuje kolejnych słuchaczy
alarmButton.addEventListener('click', () => console.log('Powiadom ochronę'));
alarmButton.addEventListener('click', () => console.log('Zapisz w dzienniku'));Co wypisze jedno kliknięcie? Trzy linie: "Alarm w sektorze B", "Powiadom ochronę" i "Zapisz w dzienniku". Komunikat o sektorze A przepadł bez śladu, bo drugie przypisanie go nadpisało. Dlatego w całym projekcie trzymaj się addEventListener(), który nikomu niczego nie odbiera.
Krótka checklista na koniec
Poza pięcioma zasadami z kodu warto pamiętać o kilku kolejnych:
- Listenery, które planujesz usunąć, trzymaj w nazwanej funkcji lub zmiennej, bo funkcji bez referencji nie zdejmiesz przez
removeEventListener() - Częste wydarzenia, jak
scroll,resizeczyinput, ograniczaj przez throttling albo debouncing - Listenerom dotyku i kółka myszy, które nie wywołują
preventDefault(), dodawaj opcję{ passive: true } - Do klikalnych elementów używaj
<button>, a nie<div>
Ostatni punkt łatwo przeoczyć. Przycisk dostaje od przeglądarki obsługę klawiatury za darmo: po wciśnięciu Enter albo spacji wysyła to samo wydarzenie click co mysz. Klikalny div wymagałby ręcznego dopisania tej obsługi, a zwykle ktoś o tym zapomina.
Moja rada: zanim oddasz kod, przejdź przez tę listę jak przez poranną inspekcję ogrodzeń. Zajmuje minutę, a oszczędza godziny szukania błędów. W piaskownicy poniżej zobaczysz dobre i złe wersje tych samych rozwiązań obok siebie. W następnej lekcji wejdziemy w zaawansowane techniki, zaczynając od gestów dotykowych.
Pamiętaj: w parku nie ma drobnych zaniedbań, a każdy nieobsłużony null może otworzyć bramę, której nikt nie pilnuje.
Kod do tej lekcji: index.html
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Dobre Praktyki Event Handling</title>
<style>
body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 20px; }
.demo-box { background: rgba(0,0,0,0.7); padding: 20px; border-radius: 10px; margin: 20px 0; }
.good { border-left: 4px solid #27ae60; }
.bad { border-left: 4px solid #e74c3c; }
button { padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer; margin: 5px; }
code { background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 3px; }
</style>
</head>
<body>
<h1>Dobre Praktyki Event Handling</h1>
<div class="demo-box good">
<h3>Dobre: Sprawdzenie istnienia elementu</h3>
<button id="good-btn">Kliknij mnie</button>
<p>Kod używa bezpiecznego pattern <code>element?.addEventListener</code></p>
</div>
<div class="demo-box good">
<h3>Dobre: Delegacja eventów</h3>
<div id="dino-container">
<button class="dino-btn" data-dino="trex">T-Rex</button>
<button class="dino-btn" data-dino="velociraptor">Velociraptor</button>
</div>
</div>
<div class="demo-box good">
<h3>Dobre: Czyszczenie listenerów</h3>
<button id="cleanup-test">Test Cleanup</button>
<p id="cleanup-status">Status: Nieaktywny</p>
</div>
<script>
console.log('Demonstracja dobrych praktyk');
// DOBRA PRAKTYKA 1: Sprawdzanie istnienia
const goodBtn = document.getElementById('good-btn');
if (goodBtn) {
goodBtn.addEventListener('click', () => {
console.log('Element exists - safe click');
alert('Bezpieczne kliknięcie!');
});
}
// DOBRA PRAKTYKA 2: Event Delegation
const dinoContainer = document.getElementById('dino-container');
dinoContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('dino-btn')) {
const dinoName = e.target.dataset.dino;
console.log(`Delegated click: ${dinoName}`);
alert(`Wybrałeś: ${dinoName}`);
}
});
// DOBRA PRAKTYKA 3: Proper Cleanup
class ComponentWithCleanup {
constructor(element) {
this.element = element;
this.handleClick = this.handleClick.bind(this);
this.init();
}
init() {
this.element.addEventListener('click', this.handleClick);
console.log('Component initialized');
}
handleClick() {
console.log('Component clicked');
document.getElementById('cleanup-status').textContent = 'Status: Aktywny';
}
destroy() {
this.element.removeEventListener('click', this.handleClick);
console.log('Component cleaned up properly');
document.getElementById('cleanup-status').textContent = 'Status: Wyczyszczony';
}
}
const cleanupBtn = document.getElementById('cleanup-test');
const component = new ComponentWithCleanup(cleanupBtn);
// Symulacja cleanup po 5 sekundach
setTimeout(() => {
component.destroy();
}, 5000);
// DOBRA PRAKTYKA 4: preventDefault świadomie
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
console.log('Prevented default save, implemented custom logic');
alert('Zapisano stan parku!');
}
});
console.log('Wszystkie dobre praktyki zaimplementowane');
</script>
</body>
</html>Hasła z dokumentacji w tej lekcji
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Dlaczego przed wywołaniem
addEventListenerwarto sprawdzić, czy element istnieje?2. Po co wywołujemy
e.preventDefault()w handlerze zdarzeniasubmitformularza?
Zadania praktyczne w grze
- Klikanie w kolejności
Ułóż warunek delegacji wydarzeń sprawdzający kliknięty element: