"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."
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.
"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"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"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}"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);"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}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%!');"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"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();"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:
closest() do znajdowania docelowych elementówreturn po obsłużeniu wydarzenia