Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Delegacja wydarzeń (Event Delegation)

"W Parku Jurajskim," Ray Arnold wskazuje na centralny panel monitoringu, "nie instalujemy osobnego systemu alarmowego przy każdym dinozaurze. To byłoby kosztowne i nieefektywne. Zamiast tego mamy centralny system monitoringu sektorów, który obsługuje wszystkie zagrody w swoim obszarze."

Ray otwiera schemat systemu. "Event delegation działa na tej samej zasadzie. Zamiast przypisywać setki event listenerów do poszczególnych elementów, przypisujemy jeden listener do ich wspólnego rodzica. Ten rodzic 'deleguje' obsługę do właściwych elementów."

Czym jest event delegation?

Event delegation to technika wykorzystująca propagację wydarzeń (bubbling) do obsługi zdarzeń na wielu elementach przez jeden listener przypisany do ich wspólnego przodka. To jedna z najważniejszych i najwydajniejszych technik w JavaScript.

Problem: Wiele event listenerów

"Zobaczmy problem," mówi Ray, pokazując nieefektywny kod.

1// ❌ NIEEFEKTYWNE - osobny listener dla każdej karty
2const dinosaurCards = document.querySelectorAll('.dinosaur-card');
3
4console.log(`Przypisuję listenery do ${dinosaurCards.length} kart dinozaurów`);
5
6dinosaurCards.forEach((card, index) => {
7  card.addEventListener('click', function() {
8    const dinoId = this.dataset.dinoId;
9    const dinoName = this.dataset.dinoName;
10
11    console.log(`Kliknięto dinozaura: ${dinoName} (ID: ${dinoId})`);
12    showDinosaurDetails(dinoId);
13  });
14
15  console.log(`Listener ${index + 1}/${dinosaurCards.length} dodany`);
16});
17
18// Problemy tego podejścia:
19// 1. Jeśli mamy 100 kart = 100 listenerów w pamięci
20// 2. Dynamicznie dodane karty NIE będą miały listenerów
21// 3. Trzeba ręcznie usuwać listenery przy usuwaniu kart
22// 4. Większe zużycie pamięci i wolniejsza strona

Rozwiązanie: Event Delegation

"Teraz obserwuj," Ray pokazuje lepsze rozwiązanie.

1// ✅ EFEKTYWNE - jeden listener na kontenerze
2const dinosaurGallery = document.getElementById('dinosaur-gallery');
3
4dinosaurGallery.addEventListener('click', function(event) {
5  // Znajdź najbliższą kartę dinozaura (nawet jeśli kliknięto w dziecko karty)
6  const card = event.target.closest('.dinosaur-card');
7
8  // Jeśli nie kliknięto w kartę dinozaura, zignoruj
9  if (!card) return;
10
11  // Upewnij się, że karta jest dzieckiem naszej galerii
12  if (!dinosaurGallery.contains(card)) return;
13
14  const dinoId = card.dataset.dinoId;
15  const dinoName = card.dataset.dinoName;
16
17  console.log(`Kliknięto dinozaura: ${dinoName} (ID: ${dinoId})`);
18  showDinosaurDetails(dinoId);
19});
20
21// Zalety tego podejścia:
22// 1. Tylko JEDEN listener niezależnie od liczby kart
23// 2. Automatycznie obsługuje dynamicznie dodane karty
24// 3. Lepsze zarządzanie pamięcią
25// 4. Łatwiejsze utrzymanie kodu

Praktyczny przykład - System kart dinozaurów

"Zbudujmy kompletny system," mówi Ray, otwierając edytor.

1// HTML struktura:
2// <div id="dino-gallery">
3//   <div class="dinosaur-card" data-dino-id="1" data-dino-name="T-Rex">
4//     <img src="trex.jpg" alt="T-Rex">
5//     <h3>Tyrannosaurus Rex</h3>
6//     <div class="card-actions">
7//       <button class="view-btn">Szczegóły</button>
8//       <button class="feed-btn">Nakarm</button>
9//       <button class="favorite-btn">★</button>
10//     </div>
11//   </div>
12//   ... więcej kart
13// </div>
14
15const gallery = document.getElementById('dino-gallery');
16
17gallery.addEventListener('click', function(event) {
18  const target = event.target;
19
20  // Obsługa przycisku szczegółów
21  if (target.classList.contains('view-btn')) {
22    const card = target.closest('.dinosaur-card');
23    const dinoId = card.dataset.dinoId;
24    const dinoName = card.dataset.dinoName;
25
26    console.log(`Otwieranie szczegółów dla: ${dinoName}`);
27    openDinosaurModal(dinoId);
28    return; // Zatrzymaj dalsze sprawdzanie
29  }
30
31  // Obsługa przycisku karmienia
32  if (target.classList.contains('feed-btn')) {
33    const card = target.closest('.dinosaur-card');
34    const dinoId = card.dataset.dinoId;
35    const dinoName = card.dataset.dinoName;
36
37    console.log(`Rozpoczynam karmienie: ${dinoName}`);
38    feedDinosaur(dinoId);
39    return;
40  }
41
42  // Obsługa przycisku ulubione
43  if (target.classList.contains('favorite-btn')) {
44    const card = target.closest('.dinosaur-card');
45    const dinoId = card.dataset.dinoId;
46    const dinoName = card.dataset.dinoName;
47
48    // Toggle stanu ulubionego
49    target.classList.toggle('active');
50
51    if (target.classList.contains('active')) {
52      console.log(`Dodano do ulubionych: ${dinoName}`);
53      addToFavorites(dinoId);
54      target.textContent = '★';
55    } else {
56      console.log(`Usunięto z ulubionych: ${dinoName}`);
57      removeFromFavorites(dinoId);
58      target.textContent = '☆';
59    }
60    return;
61  }
62
63  // Obsługa kliknięcia w samą kartę (ale nie w przyciski)
64  const card = target.closest('.dinosaur-card');
65  if (card && gallery.contains(card)) {
66    const dinoId = card.dataset.dinoId;
67    const dinoName = card.dataset.dinoName;
68
69    console.log(`Kliknięto kartę: ${dinoName}`);
70    highlightCard(card);
71  }
72});
73
74function openDinosaurModal(dinoId) {
75  console.log(`Ładowanie danych dinozaura ID: ${dinoId}`);
76  // Logika otwierania modala
77}
78
79function feedDinosaur(dinoId) {
80  console.log(`Aktywacja systemu karmienia dla dinozaura ID: ${dinoId}`);
81  console.log('Sprawdzanie bezpieczeństwa...');
82  console.log('Otwieranie podajnika...');
83  console.log('Karmienie zakończone');
84}
85
86function addToFavorites(dinoId) {
87  const favorites = JSON.parse(localStorage.getItem('favoriteDinosaurs') || '[]');
88  if (!favorites.includes(dinoId)) {
89    favorites.push(dinoId);
90    localStorage.setItem('favoriteDinosaurs', JSON.stringify(favorites));
91    console.log('Zapisano w ulubionych');
92  }
93}
94
95function removeFromFavorites(dinoId) {
96  let favorites = JSON.parse(localStorage.getItem('favoriteDinosaurs') || '[]');
97  favorites = favorites.filter(id => id !== dinoId);
98  localStorage.setItem('favoriteDinosaurs', JSON.stringify(favorites));
99  console.log('Usunięto z ulubionych');
100}
101
102function highlightCard(card) {
103  // Usuń highlight z innych kart
104  document.querySelectorAll('.dinosaur-card.highlighted').forEach(c => {
105    c.classList.remove('highlighted');
106  });
107
108  // Dodaj highlight do klikniętej karty
109  card.classList.add('highlighted');
110  console.log('Karta podświetlona');
111}

Dynamiczne dodawanie elementów

"Największa zaleta delegation," Ray dodaje nowe elementy do interfejsu, "to automatyczna obsługa dynamicznie dodawanych elementów."

1const gallery = document.getElementById('dino-gallery');
2
3// Event delegation - działa dla wszystkich kart, nawet przyszłych
4gallery.addEventListener('click', function(event) {
5  const card = event.target.closest('.dinosaur-card');
6  if (!card) return;
7
8  const dinoName = card.dataset.dinoName;
9  console.log(`Kliknięto: ${dinoName}`);
10});
11
12// Funkcja dodająca nowe karty dynamicznie
13function addNewDinosaur(dinosaur) {
14  const newCard = document.createElement('div');
15  newCard.className = 'dinosaur-card';
16  newCard.dataset.dinoId = dinosaur.id;
17  newCard.dataset.dinoName = dinosaur.name;
18
19  newCard.innerHTML = `
20    <img src="${dinosaur.image}" alt="${dinosaur.name}">
21    <h3>${dinosaur.name}</h3>
22    <p>${dinosaur.species}</p>
23    <div class="card-actions">
24      <button class="view-btn">Szczegóły</button>
25      <button class="feed-btn">Nakarm</button>
26      <button class="favorite-btn"></button>
27    </div>
28  `;
29
30  gallery.appendChild(newCard);
31  console.log(`Dodano nowego dinozaura: ${dinosaur.name}`);
32
33  // NIE TRZEBA dodawać event listenerów!
34  // Delegation automatycznie obsłuży nową kartę!
35}
36
37// Przykład użycia
38setTimeout(() => {
39  addNewDinosaur({
40    id: '15',
41    name: 'Spinosaurus',
42    species: 'Spinosaurus aegyptiacus',
43    image: 'spinosaurus.jpg'
44  });
45
46  // Nowa karta już działa z kliknięciami!
47}, 3000);

Zaawansowana delegacja - filtry i warunki

"Czasami," wyjaśnia Ray, "potrzebujemy bardziej precyzyjnej kontroli."

1const parkDashboard = document.getElementById('park-dashboard');
2
3parkDashboard.addEventListener('click', function(event) {
4  const target = event.target;
5
6  // Delegacja z atrybutem data-action
7  const action = target.dataset.action;
8
9  if (action) {
10    console.log(`Akcja: ${action}`);
11
12    switch(action) {
13      case 'open-gate':
14        handleOpenGate(target);
15        break;
16
17      case 'close-gate':
18        handleCloseGate(target);
19        break;
20
21      case 'start-feeding':
22        handleStartFeeding(target);
23        break;
24
25      case 'emergency-protocol':
26        handleEmergencyProtocol(target);
27        break;
28
29      case 'view-stats':
30        handleViewStats(target);
31        break;
32
33      default:
34        console.log('Nieznana akcja:', action);
35    }
36
37    return;
38  }
39
40  // Delegacja z klasami
41  if (target.matches('.enclosure-zone')) {
42    const enclosureId = target.dataset.enclosureId;
43    console.log(`Kliknięto strefę zagrody: ${enclosureId}`);
44    showEnclosureDetails(enclosureId);
45    return;
46  }
47
48  // Delegacja z tagami
49  if (target.tagName === 'A' && target.classList.contains('internal-link')) {
50    event.preventDefault();
51    const href = target.getAttribute('href');
52    console.log(`Nawigacja wewnętrzna: ${href}`);
53    navigateInternal(href);
54    return;
55  }
56});
57
58function handleOpenGate(button) {
59  const gateId = button.dataset.gateId;
60  const enclosureId = button.dataset.enclosureId;
61
62  console.log(`Otwieranie bramy ${gateId} w zagrodzie ${enclosureId}`);
63  console.log('Sprawdzanie statusu bezpieczeństwa...');
64  console.log('Brama otwarta');
65}
66
67function handleCloseGate(button) {
68  const gateId = button.dataset.gateId;
69  console.log(`Zamykanie bramy ${gateId}`);
70  console.log('Brama zamknięta i zabezpieczona');
71}
72
73function handleStartFeeding(button) {
74  const enclosureId = button.dataset.enclosureId;
75  const feedType = button.dataset.feedType;
76
77  console.log(`Rozpoczęcie karmienia w zagrodzie ${enclosureId}`);
78  console.log(`Typ pokarmu: ${feedType}`);
79}
80
81function handleEmergencyProtocol(button) {
82  const protocolId = button.dataset.protocolId;
83
84  console.log('🚨 ALARM! Aktywacja protokołu awaryjnego');
85  console.log(`Protokół: ${protocolId}`);
86  console.log('Zamykanie wszystkich bram...');
87  console.log('Powiadamianie personelu...');
88}

Wydajność - porównanie

Ray pokazuje testy wydajności. "Liczby nie kłamią."

1// Test 1: Bez delegation (osobne listenery)
2console.time('Przypisywanie bez delegation');
3
4const cards1 = document.querySelectorAll('.card');
5cards1.forEach(card => {
6  card.addEventListener('click', function() {
7    console.log('Kliknięto');
8  });
9});
10
11console.timeEnd('Przypisywanie bez delegation');
12// Typowo: 5-15ms dla 100 elementów
13
14// Test 2: Z delegation (jeden listener)
15console.time('Przypisywanie z delegation');
16
17const container = document.getElementById('container');
18container.addEventListener('click', function(event) {
19  if (event.target.matches('.card')) {
20    console.log('Kliknięto');
21  }
22});
23
24console.timeEnd('Przypisywanie z delegation');
25// Typowo: < 1ms niezależnie od liczby elementów
26
27// Zużycie pamięci:
28// Bez delegation: ~1-2KB na listener × liczba elementów
29// Z delegation: ~1-2KB total
30
31console.log('Różnica w pamięci dla 100 elementów:');
32console.log('Bez delegation: ~100-200KB');
33console.log('Z delegation: ~1-2KB');
34console.log('Oszczędność: 98-99%!');

matches() vs closest() - wybór metody

"Dwie najważniejsze metody dla delegation," wyjaśnia Ray.

1const container = document.getElementById('enclosure-list');
2
3container.addEventListener('click', function(event) {
4  const target = event.target;
5
6  // matches() - sprawdza czy SAM element pasuje
7  if (target.matches('.enclosure-item')) {
8    console.log('Kliknięto BEZPOŚREDNIO w .enclosure-item');
9    // Zadziała tylko jeśli kliknięto dokładnie w element z klasą
10  }
11
12  // closest() - szuka w górę do pierwszego pasującego
13  const item = target.closest('.enclosure-item');
14  if (item) {
15    console.log('Kliknięto w .enclosure-item LUB jego dziecko');
16    // Zadziała jeśli kliknięto w element lub jego dzieci
17  }
18});
19
20// HTML:
21// <div class="enclosure-item">
22//   <h3>Zagroda T-Rex</h3>           ← matches() NIE, closest() TAK
23//   <p>Status: Aktywna</p>           ← matches() NIE, closest() TAK
24//   <button>Szczegóły</button>       ← matches() NIE, closest() TAK
25// </div>                             ← matches() TAK, closest() TAK
26
27// WNIOSEK: Używaj closest() dla delegation - jest bardziej elastyczne

Kompletny system zarządzania parkiem

"Połączmy wszystko w działający system," mówi Ray.

1class JurassicParkManagementSystem {
2  constructor() {
3    this.selectedEnclosures = new Set();
4    this.initialize();
5  }
6
7  initialize() {
8    console.log('Inicjalizacja systemu zarządzania parkiem');
9    this.setupDelegation();
10  }
11
12  setupDelegation() {
13    // Jeden główny listener dla całego dashboardu
14    const dashboard = document.getElementById('park-dashboard');
15
16    dashboard.addEventListener('click', (event) => {
17      this.handleDashboardClick(event);
18    });
19
20    console.log('System delegation aktywny');
21  }
22
23  handleDashboardClick(event) {
24    const target = event.target;
25
26    // Obsługa różnych typów kliknięć
27    this.handleEnclosureSelection(target) ||
28    this.handleActionButton(target) ||
29    this.handleTabNavigation(target) ||
30    this.handleStatusIndicator(target);
31  }
32
33  handleEnclosureSelection(target) {
34    const enclosure = target.closest('.enclosure-card');
35    if (!enclosure) return false;
36
37    const enclosureId = enclosure.dataset.enclosureId;
38    const enclosureName = enclosure.dataset.enclosureName;
39
40    console.log(`Wybrano zagrodę: ${enclosureName} (ID: ${enclosureId})`);
41
42    // Toggle selekcji
43    if (this.selectedEnclosures.has(enclosureId)) {
44      this.selectedEnclosures.delete(enclosureId);
45      enclosure.classList.remove('selected');
46      console.log('Usunięto z selekcji');
47    } else {
48      this.selectedEnclosures.add(enclosureId);
49      enclosure.classList.add('selected');
50      console.log('Dodano do selekcji');
51    }
52
53    this.updateSelectionDisplay();
54    return true;
55  }
56
57  handleActionButton(target) {
58    const action = target.dataset.action;
59    if (!action) return false;
60
61    const enclosure = target.closest('.enclosure-card');
62    const enclosureId = enclosure?.dataset.enclosureId;
63
64    console.log(`Akcja: ${action} dla zagrody: ${enclosureId}`);
65
66    const actions = {
67      'feed': () => this.feedDinosaur(enclosureId),
68      'clean': () => this.cleanEnclosure(enclosureId),
69      'maintenance': () => this.scheduleMainenance(enclosureId),
70      'emergency': () => this.activateEmergency(enclosureId)
71    };
72
73    const handler = actions[action];
74    if (handler) {
75      handler();
76      return true;
77    }
78
79    return false;
80  }
81
82  handleTabNavigation(target) {
83    if (!target.classList.contains('tab-button')) return false;
84
85    const tabName = target.dataset.tab;
86    console.log(`Przełączanie na kartę: ${tabName}`);
87
88    // Usuń active ze wszystkich tabów
89    document.querySelectorAll('.tab-button').forEach(tab => {
90      tab.classList.remove('active');
91    });
92
93    // Dodaj active do klikniętego
94    target.classList.add('active');
95
96    // Pokaż odpowiednią zawartość
97    this.showTabContent(tabName);
98    return true;
99  }
100
101  handleStatusIndicator(target) {
102    if (!target.classList.contains('status-indicator')) return false;
103
104    const enclosure = target.closest('.enclosure-card');
105    const enclosureId = enclosure?.dataset.enclosureId;
106
107    console.log(`Pokazywanie szczegółów statusu dla zagrody: ${enclosureId}`);
108    this.showStatusDetails(enclosureId);
109    return true;
110  }
111
112  feedDinosaur(enclosureId) {
113    console.log(`Karmienie dinozaura w zagrodzie: ${enclosureId}`);
114  }
115
116  cleanEnclosure(enclosureId) {
117    console.log(`Czyszczenie zagrody: ${enclosureId}`);
118  }
119
120  scheduleMainenance(enclosureId) {
121    console.log(`Planowanie konserwacji dla zagrody: ${enclosureId}`);
122  }
123
124  activateEmergency(enclosureId) {
125    console.log(`🚨 ALARM! Protokół awaryjny dla zagrody: ${enclosureId}`);
126  }
127
128  showTabContent(tabName) {
129    console.log(`Ładowanie zawartości karty: ${tabName}`);
130  }
131
132  showStatusDetails(enclosureId) {
133    console.log(`Szczegóły statusu zagrody: ${enclosureId}`);
134  }
135
136  updateSelectionDisplay() {
137    const count = this.selectedEnclosures.size;
138    const display = document.getElementById('selection-counter');
139
140    if (display) {
141      display.textContent = `Wybrano: ${count} zagród`;
142    }
143
144    console.log(`Aktualnie wybrane zagrody: ${Array.from(this.selectedEnclosures).join(', ')}`);
145  }
146}
147
148// Inicjalizacja systemu
149const parkSystem = new JurassicParkManagementSystem();

Podsumowanie

"Event delegation," kończy Ray, "to nie tylko optymalizacja. To fundamentalna technika w nowoczesnym JavaScript. Jeden listener obsługujący tysiące elementów - to jak centralny system monitoringu obsługujący cały park. Wydajne, skalowalne i łatwe w utrzymaniu."

Kluczowe zasady:

  • Używaj
    closest()
    do znajdowania docelowych elementów
  • Sprawdzaj czy element jest dzieckiem kontenera z listenerem
  • Używaj data-atrybutów dla akcji i identyfikatorów
  • Pamiętaj o
    return
    po obsłużeniu wydarzenia
  • Delegation działa automatycznie z dynamicznymi elementami
  • Oszczędza pamięć i poprawia wydajność
Vai a CodeWorlds