Gdy dopiero poznawałem tajniki tworzenia stron internetowych, jednym z najbardziej zaskakujących i jednocześnie fascynujących zjawisk, z jakimi się zetknąłem, było tak zwane „bulgotanie” zdarzeń. Początkowo wydawało się to nieco skomplikowane, jednak po zrozumieniu idei propagacji zdarzeń, wszystko nabrało sensu. Każdy twórca stron internetowych prędzej czy później spotka się z tym mechanizmem. Czym więc dokładnie jest to bulgotanie zdarzeń?
Aby umożliwić interakcję użytkowników ze stronami internetowymi, JavaScript wykorzystuje zdarzenia. Zdarzenie to nic innego jak akcja lub sytuacja, która może zostać wykryta i na którą możemy zareagować za pomocą kodu. Do przykładowych zdarzeń zaliczamy kliknięcia myszą, naciśnięcia klawiszy czy wysłanie formularza.
JavaScript używa tak zwanych detektorów zdarzeń do wyłapywania tych akcji i reagowania na nie. Detektor zdarzeń to funkcja, która czeka na wystąpienie konkretnego zdarzenia na stronie, na przykład kliknięcie przycisku. Gdy detektor wykryje zdarzenie, na które nasłuchuje, reaguje, wykonując przypisany do niego kod. Cały ten proces, od wykrycia zdarzenia po reakcję na nie, nazywamy obsługą zdarzeń.
Wyobraźmy sobie teraz sytuację, w której na naszej stronie znajdują się trzy elementy: div, span oraz przycisk. Przycisk jest umieszczony wewnątrz elementu span, który z kolei znajduje się w elemencie div. Poniżej znajduje się wizualizacja takiej struktury:
Załóżmy, że każdy z tych elementów ma przypisany detektor zdarzeń, który reaguje na kliknięcie i wyświetla odpowiednią wiadomość w konsoli. Co się stanie, gdy klikniemy przycisk?
Aby to sprawdzić samodzielnie, utwórz nowy folder, a w nim plik HTML o nazwie index.html, plik CSS o nazwie style.css oraz plik JavaScript o nazwie app.js.
W pliku HTML wstaw następujący kod:
<!DOCTYPE html> <html lang="en"> <head> <title>Event bubbling</title> <link rel="stylesheet" href="https://wilku.top/the-hidden-key-to-dynamic-web-interactions/style.css"> </head> <body> <div> <span><button>Click Me!</button></span> </div> <script src="app.js"></script> </body> </html>
W pliku CSS dodaj poniższy kod, aby ostylować elementy div i span:
div { border: 2px solid black; background-color: orange; padding: 30px; width: 400px; } span { display: inline-block; background-color: cyan; height: 100px; width: 200px; margin: 10px; padding: 20px; border: 2px solid black; }
W pliku JavaScript dodaj poniższy kod, który doda detektory zdarzeń do elementów div, span i przycisk. Każdy detektor będzie reagować na kliknięcie:
const div = document.querySelector('div'); div.addEventListener('click', () => { console.log("You've clicked a div element") }) const span = document.querySelector('span'); span.addEventListener('click', () => { console.log("You've clicked a span element") }) const button = document.querySelector('button'); button.addEventListener('click', () => { console.log("You've clicked a button") })
Teraz otwórz plik HTML w przeglądarce. Zajrzyj do konsoli i kliknij przycisk na stronie. Co zauważasz? Wynik kliknięcia przycisku przedstawiono poniżej:
Kliknięcie przycisku aktywuje detektor zdarzeń przypisany do przycisku, który wyświetla odpowiedni komunikat w konsoli. Jednakże, aktywowane zostają również detektory zdarzeń elementów span i div. Dlaczego tak się dzieje?
Kliknięcie przycisku uruchamia detektor zdarzeń przypisany do niego, co powoduje wyświetlenie odpowiedniej informacji. Ponieważ jednak przycisk jest zagnieżdżony w elemencie span, kliknięcie przycisku oznacza jednocześnie kliknięcie elementu span, a co za tym idzie, uruchomienie jego detektora zdarzeń.
Z kolei element span jest umieszczony w elemencie div, więc kliknięcie span powoduje również kliknięcie diva, uruchamiając i jego detektor zdarzeń. To właśnie jest bulgotanie zdarzeń.
Bulgotanie wydarzenia
Bulgotanie zdarzeń to proces, w którym zdarzenie, które wystąpi w zagnieżdżonym elemencie HTML, rozprzestrzenia się od elementu najbardziej wewnętrznego, gdzie zostało wywołane, w górę drzewa DOM, aż do elementu głównego, aktywując po drodze wszystkie detektory zdarzeń, które nasłuchują tego zdarzenia.
Detektory zdarzeń są wyzwalane w określonej kolejności, która odpowiada kolejności rozchodzenia się zdarzenia w drzewie DOM. Spójrzmy na poniższe drzewo DOM, które obrazuje strukturę kodu HTML użytego w przykładzie.
Zdarzenie kliknięcia pojawiające się w drzewie DOM
Drzewo DOM przedstawia przycisk zagnieżdżony wewnątrz elementu span, który znajduje się w elemencie div, a ten z kolei w elemencie body, który jest zagnieżdżony w elemencie html. Po kliknięciu przycisku, zdarzenie click uruchomi najpierw detektor zdarzeń przypisany do przycisku.
Jednak ze względu na zagnieżdżenie elementów, zdarzenie „wędruje” w górę drzewa DOM (czyli „bąbelkuje” w górę), przechodząc kolejno przez element span, potem div, następnie body i na końcu element html, uruchamiając po drodze wszystkie detektory zdarzeń, które nasłuchują na zdarzenie kliknięcia.
Właśnie dlatego uruchamiane są również detektory zdarzeń przypisane do elementów span i div. Gdybyśmy mieli detektory zdarzeń na elemencie body i html, one również zostałyby wywołane.
Węzeł DOM, w którym faktycznie występuje zdarzenie, nazywamy celem zdarzenia. W naszym przykładzie, ponieważ kliknięcie następuje na przycisku, celem zdarzenia jest właśnie element przycisku.
Jak zatrzymać bulgotanie wydarzeń
Aby zapobiec rozprzestrzenianiu się zdarzenia w drzewie DOM, używamy metody o nazwie `stopPropagation()`, która jest dostępna w obiekcie zdarzenia. Spójrzmy na przykładowy kod, który wykorzystaliśmy do dodania detektora zdarzeń do przycisku:
const button = document.querySelector('button'); button.addEventListener('click', () => { console.log("You've clicked a button"); })
Ten kod powoduje, że po kliknięciu przycisku zdarzenie „bąbelkuje” w drzewie DOM. Aby zapobiec temu procesowi, wywołujemy metodę `stopPropagation()`, jak pokazano poniżej:
const button = document.querySelector('button'); button.addEventListener('click', (e) => { console.log("You've clicked a button"); e.stopPropagation(); })
Funkcja obsługi zdarzeń jest wykonywana po kliknięciu przycisku. Detektor zdarzeń automatycznie przekazuje obiekt zdarzenia do tej funkcji. W naszym przykładzie obiekt zdarzenia jest reprezentowany przez zmienną `e`, która jest parametrem funkcji obsługi zdarzenia.
Obiekt zdarzenia `e` zawiera informacje o zdarzeniu i umożliwia dostęp do różnych właściwości i metod z nim związanych. Jedną z tych metod jest właśnie `stopPropagation()`, która służy do zatrzymywania propagacji zdarzenia. Wywołanie tej funkcji w detektorze zdarzeń przycisku zapobiega „wędrowaniu” zdarzenia w górę drzewa DOM.
Wynik kliknięcia przycisku po dodaniu metody `stopPropagation()` przedstawiono poniżej:
Używając funkcji `stopPropagation()`, możemy zatrzymać rozprzestrzenianie się zdarzenia z konkretnego elementu, w którym ją zastosowaliśmy. Na przykład, jeśli chcemy, aby zdarzenie kliknięcia „wędrowało” tylko z przycisku do elementu span, ale nie dalej, to powinniśmy użyć `stopPropagation()` w detektorze zdarzeń elementu span.
Przechwytywanie zdarzeń
Przechwytywanie zdarzeń jest przeciwieństwem bulgotania zdarzeń. W tym przypadku zdarzenie „spływa” od elementu najbardziej zewnętrznego do elementu docelowego, jak pokazano na poniższym obrazku:
Zdarzenie kliknięcia „spływające” do elementu docelowego w wyniku przechwytywania zdarzeń
W naszym przykładzie, po kliknięciu przycisku, podczas przechwytywania zdarzeń, jako pierwsze zostaną uruchomione detektory zdarzeń na elemencie div. Następnie aktywowane zostaną detektory elementu span, a na końcu detektory elementu docelowego.
Jednak propagacja zdarzeń (bulgotanie) jest domyślnym sposobem rozprzestrzeniania się zdarzeń w modelu DOM. Aby zmienić to zachowanie na przechwytywanie zdarzeń, musimy przekazać trzeci argument do naszych detektorów zdarzeń, ustawiając wartość „capture” na `true`. Jeśli nie przekażemy tego argumentu, przechwytywanie zdarzeń domyślnie będzie miało wartość `false`.
Spójrzmy na poniższy detektor zdarzeń:
div.addEventListener('click', () => { console.log("You've clicked a div element") })
Ponieważ nie ma trzeciego argumentu, przechwytywanie ma wartość `false`. Aby ustawić przechwytywanie na `true`, przekazujemy jako trzeci argument wartość `true`.
div.addEventListener('click', () => { console.log("You've clicked a div element") }, true)
Możemy też przekazać obiekt, w którym ustawiamy przechwytywanie na `true`:
div.addEventListener('click', () => { console.log("You've clicked a div element") }, {capture: true})
Aby przetestować przechwytywanie zdarzeń, w pliku JavaScript dodaj trzeci argument do wszystkich detektorów, jak pokazano poniżej:
const div = document.querySelector('div'); div.addEventListener('click', () => { console.log("You've clicked a div element") }, true) const span = document.querySelector('span'); span.addEventListener('click', (e) => { console.log("You've clicked a span element") }, true) const button = document.querySelector('button'); button.addEventListener('click', () => { console.log("You've clicked a button"); }, true)
Teraz otwórz przeglądarkę i kliknij element przycisku. Powinieneś otrzymać następujący wynik:
Zauważ, że w przeciwieństwie do bulgotania zdarzeń, gdzie pierwszy komunikat pochodził z przycisku, w przypadku przechwytywania, pierwszy komunikat pochodzi z elementu najbardziej zewnętrznego, czyli div.
Bulgotanie i przechwytywanie zdarzeń to główne sposoby propagacji zdarzeń w DOM. Jednak propagacja (bulgotanie) zdarzeń jest najczęściej stosowanym mechanizmem.
Delegacja Wydarzenia
Delegowanie zdarzeń to technika, w której pojedynczy detektor zdarzeń jest przypisywany do wspólnego elementu nadrzędnego, na przykład do elementu `
- `, zamiast do każdego elementu podrzędnego. Zdarzenia pochodzące z elementów podrzędnych są przekazywane do elementu nadrzędnego, gdzie są obsługiwane przez funkcję obsługi zdarzeń tego elementu.
Czyli, gdy mamy element nadrzędny z elementami podrzędnymi, to detektor zdarzeń dodajemy tylko do elementu nadrzędnego. Ta funkcja obsłuży wszystkie zdarzenia pochodzące z elementów podrzędnych.
Możesz się zastanawiać, skąd element nadrzędny wie, który element podrzędny został kliknięty. Jak wspomniano wcześniej, detektor zdarzeń przekazuje obiekt zdarzenia do funkcji obsługi. Ten obiekt zdarzenia ma właściwości i metody, które dostarczają informacji o konkretnym zdarzeniu. Jedną z tych właściwości jest `target`. Właściwość `target` wskazuje konkretny element HTML, w którym wystąpiło zdarzenie.
Na przykład, jeśli mamy nieuporządkowaną listę z elementami `
- `, to po wystąpieniu zdarzenia w elemencie `
- `, właściwość `target` w obiekcie zdarzenia wskaże konkretny element `
- `, w którym zdarzenie nastąpiło.
Aby zobaczyć delegowanie zdarzeń w akcji, dodaj poniższy kod HTML do istniejącego pliku HTML:
<ul> <li>Toyota</li> <li>Subaru</li> <li>Honda</li> <li>Hyundai</li> <li>Chevrolet</li> <li>Kia</li> </ul>
Dodaj poniższy kod JavaScript, aby użyć delegowania zdarzeń, wykorzystując pojedynczy detektor na elemencie nadrzędnym do nasłuchiwania zdarzeń na elementach podrzędnych:
const ul = document.querySelector('ul'); ul.addEventListener('click', (e) => { // element, który był celem zdarzenia targetElement = e.target // wypisz zawartość elementu console.log(targetElement.textContent) })
Teraz otwórz przeglądarkę i kliknij dowolny element listy. Zawartość klikniętego elementu powinna zostać wypisana w konsoli, jak poniżej:
Używając pojedynczego detektora zdarzeń, możemy obsłużyć zdarzenia we wszystkich elementach potomnych. Zbyt duża liczba detektorów zdarzeń na stronie wpływa negatywnie na jej wydajność, zużywa więcej pamięci i prowadzi do wolniejszego ładowania i renderowania strony.
Delegowanie zdarzeń pozwala nam tego uniknąć, minimalizując liczbę detektorów potrzebnych na stronie. Delegowanie zdarzeń opiera się na propagacji (bulgotaniu) zdarzeń. Dlatego można powiedzieć, że propagacja zdarzeń może pomóc w optymalizacji wydajności stron internetowych.
Wskazówki dotyczące skutecznej obsługi zdarzeń
Jako programista, pracując ze zdarzeniami w modelu DOM, rozważ użycie delegowania zdarzeń, zamiast przypisywania detektorów zdarzeń do każdego elementu strony.
Korzystając z delegowania zdarzeń, pamiętaj o dołączaniu detektora do najbliższego wspólnego elementu nadrzędnego elementów podrzędnych, które wymagają obsługi zdarzeń. Pomaga to zoptymalizować propagację zdarzeń i minimalizuje ścieżkę, którą musi przebyć zdarzenie, zanim zostanie obsłużone.
Podczas obsługi zdarzeń, wykorzystaj obiekt zdarzenia dostarczony przez detektor. Zawiera on właściwości, takie jak `target`, które są bardzo przydatne przy obsłudze zdarzeń.
Aby strony były bardziej wydajne, unikaj nadmiernej manipulacji DOM. Częste zmiany DOM mogą negatywnie wpłynąć na wydajność Twojej strony.
Na koniec, w przypadku elementów zagnieżdżonych, zachowaj szczególną ostrożność podczas przypisywania detektorów zdarzeń do zagnieżdżonych elementów. Może to nie tylko wpłynąć na wydajność, ale również sprawić, że obsługa zdarzeń stanie się bardzo skomplikowana, a kod trudny w utrzymaniu.
Wniosek
Zdarzenia są potężnym narzędziem w JavaScript. Bulgotanie zdarzeń, przechwytywanie zdarzeń i delegowanie zdarzeń są ważnymi mechanizmami w obsłudze zdarzeń w JavaScript. Jako programista stron internetowych, wykorzystaj wiedzę z tego artykułu, aby tworzyć bardziej interaktywne, dynamiczne i wydajne witryny i aplikacje.
newsblog.pl
Maciej – redaktor, pasjonat technologii i samozwańczy pogromca błędów w systemie Windows. Zna Linuxa lepiej niż własną lodówkę, a kawa to jego główne źródło zasilania. Pisze, testuje, naprawia – i czasem nawet wyłącza i włącza ponownie. W wolnych chwilach udaje, że odpoczywa, ale i tak kończy z laptopem na kolanach.