Miniatura artykułu

JavaScript - bubbling i capturing

11 minut

Skopiuj link

Data publikacji: 10/18/2023, 9:52:49

Ostatnia aktualizacja: 4/1/2024

Wstęp

Zacznijmy od szybkiego wyjaśnienia, czym w ogóle jest bubbling i capturing. Najprościej mówiąc są to nazwy faz, które występują w czasie życia eventu (specyfikacja mówi o trzech faza, ale dla uproszczenia o tej ostatniej wspomnimy dopiero w dalszej części artykułu).

Być może zastanawiasz się po co Ci w ogóle wiedza na temat cyklu życia zdarzeń, skoro bez niej też można z nich korzystać i wszystko działa jak należy.
Oczywiście jest to prawda - większość codziennych zastosowań nie wymaga dogłębnego poznania teorii, ale istnieje duże prawdopodobieństwo, że w końcu napotkasz błąd, lub nietypowe zachowanie, którego nie będziesz w stanie wyjaśnić lub zrozumieć, a wszystko to przez brak znajomości teorii.

Właśnie dlatego, niemal cały artykuł został poświęcony właśnie teorii (chociaż będzie też kilka przykładów i nieco kodu). Omówimy też kilka przydatnych metod i właściwości.

Trzy fazy zdarzenia

W czasie swojego "życia", każde zdarzenie posiada 3 fazy - capturing, targeting oraz bubbling. Żeby w pełni zrozumieć zachowanie funkcji, przypisanych do obsługi zdarzeń, oraz samo zdarzenie, należy zaznajomić się z każdą z nich.

Zanim przejdziemy do omówienia poszczególnych faz, to warto wiedzieć, że każde zdarzenie jest wysyłane z najwyższego poziomu - obiektu window. W "drodze" do elementu, który je wywołał przechodzi przez wszystkie elementy HTML, w których jest on zagnieżdżony (capturing), znajduje wskazany element (targeting), a następnie wraca z powrotem "do góry" (bubbling), aż dotrze do obiektu window.

Czas na szczegółowe opisanie każdej z wymienionych faz.

Capturing

Capturing to pierwsza faza - możemy to sprawdzić za pomocą właściwości obiektu event, który otrzymuje każda funkcja użyta do obsługi zdarzeń, a konkretnie event.eventPhase i to właśnie od niej zaczniemy.

Na początku zapoznaj się z poniższą strukturą elementów HTML:

Jest ona dość prosta - mamy 3 elementy (main, div oraz button), a każdy z nich posiada przypisany nasłuchiwacz, który reaguje na zdarzenie onclick, czyli po prostu kliknięcie danego elementu myszką.

W momencie kliknięcia na element button, czyli najbardziej zagnieżdżonego elementu, zdarzenie jest wysyłane z góry, a więc od obiektu window, przez document, oraz wszystkie elementy HTML, które znajdują się po drodze, aż do tego, który został wskazany myszką.

Ta "podróż" przez wszystkie elementy od góry do dołu jest właśnie nazywana fazą przechwytywania (capturing), ponieważ wszystkie elementy po drodze, niejako przechwytują zdarzenie, zanim trafi ono głębiej.

Jednak domyślnym zachowaniem w zdecydowanej większości przeglądarek, jest reagowanie na bubbling (czyli ostatnią fazę), a nie na capturing. Oczywiście zachowanie to można zmienić:
element.addEventListener("click", callback, { capture: true }) lub z użyciem nieco krótszego zapisu element.addEventListener("click", callback, true).

Tak dodany nasłuchwiacz będzie reagować wyłącznie na pierwszą fazę (przestanie reagować na trzecią - bubbling), co w praktyce oznacza, że logi zobaczymy w następującej kolejności:

  1. main

  2. div

  3. button

A więc od najbardziej zewnętrznego elementu, do najbardziej zagnieżdżonego. Oznacza to, że nasłuchiwacz na klikniętym elemencie zostanie wywołany na samym końcu.

Targeting

To zdecydowanie najkrótsza i najłatwiejsza do zrozumienia faza, nie ma ona też wielkiego wpływu na działanie, dlatego jest bardzo często pomijana w różnego rodzaju publikacjach. Postanowiłem o niej wspomnieć, żeby omówić temat nieco bardziej szczegółowo.

Oznaczona jest cyfrą 2, więc występuje pomiędzy capturing a bubbling. Jedyne, co w tej fazie się dzieje, to "dotarcie" zdarzenia do klikniętego elementu, który znajduje się we właściwości target w obiekcie zdarzenia, oraz wywołanie nasłuchiwaczy przypisanych do niego.

Bubbling

Bubbling to ostatnia faza (oznaczona numerem 3). Mimo, że domyślnie występuje na samym końcu, to jest najczęściej wykorzystywana i tym samym najważniejsza do zrozumienia.

Dla uproszczenia, struktura HTML pozostaje ta sama, co wyżej.

Jeżeli klikniemy na najbardziej zagnieżdżony element (button) i nie użyjemy opcji capture: true (czyli po prostu zostawimy domyślne zachowanie), to logi zobaczymy w odwrotnej kolejności - od klikniętego elementu, do najbardziej zewnętrznego:

  1. button

  2. div

  3. main

Jest to odwrotne zachowanie niż w przypadku wykorzystania fazy capturing, w której najpierw wywoływane były nasłuchiwacze na zewnętrznych elementach, a dopiero później zdarzenie wędrowało głębiej. W tym przypadku, nasłuchiwacz na klikniętym elemencie zostanie wywołany jako pierwszy.

Czas na podsumowanie wszystkich trzech faz. Żeby jeszcze lepiej zrozumieć ich działanie, stworzyłem nieco bardziej skomplikowaną strukturę:

W tym przypadku zdarzenie (click) występuje na przycisku z tekstem Right button. Poniższy diagram przedstawia wszystkie fazy, wraz z ich numerycznym odpowiednikiem:

Można zatem powiedzieć, że zdarzenie wykonuje pętlę - zaczyna od window, dociera do wskazanego elementu HTML, a następnie wraca do window.

Metoda stopPropagation

Nie zawsze chcemy, żeby zdarzenie było przekazywane do kolejnych elementów HTML. Zachowanie to można zatrzymać z użyciem metody stopPropagation dostępnej w każdym obiekcie zdarzenia.

Wywołaniej jej (niezależnie od fazy) przerywa dalszą propagację zdarzenia. W fazie capturing zdarzenie przestanie być przesyłane "w dół" do kolejnych zagnieżdżonych elementów, natomiast w fazie bubbling, przerwiemy przesyłanie "w górę".

Zerknij na schemat, oraz kod HTML powyżej. Rozbudujemy go nieco i dodamy nasłuchiwacze z wykorzystaniem JavaScriptu. Nadal klikamy w ten sam przycisk (tutaj dodałem do niego id rightButton)

Ponieważ w funkcji, która zostanie wywołana po kliknięciu w element o id container (lub w element w nim zagnieżdżony) zatrzymuję dalszą propagację zdarzenia, to nigdy nie dotrze ono do elementu nadrzędnego, czyli <main>. Więc w konsoli zobaczymy tylko trzy logi: rightButton, rightContainer oraz container.

Przerwaliśmy zatem drogę eventu w górę od elementu, który został kliknięty.

Zwróć uwagę, że przerwałem tu fazę trzecią - bubbling, ponieważ domyślnie, to właśnie w niej uruchamiane są nasłuchiwacze. Można to w prosty sposób przedstawić na diagramie:

Ten sam przypadek wyglądałby zupełnie inaczej gdybym przerwał fazę capturing. Stwórzmy identyczną strukturę, jednak zamiast fazy bubbling wykorzystamy teraz capturing i to właśnie ją przerwiemy tym razem.

Ponieważ przerywamy drogę eventu z góry na dół to nie zostaną wywołane dwa ostatnie nasłuchiwacze - te najbardziej zagnieżdżone: rightButton i rightContainer. Na diagramie będzie to wyglądać następująco:

Istnieje jeszcze jedna metoda, o której warto wiedzieć - stopImmidatePropagation. Jest ona przydatna, kiedy jeden element ma kilka nasłuchiwaczy na to samo zdarzenie.

Metoda stopPropagation przerwie wysyłanie zdarzenia do kolejnego elementu HTML, ale nie zatrzyma wywołania kolejnych nasłuchiwaczy w tym samym elemencie.

Gdyby container miał kilka różnych nasłuchiwaczy na zdarzenie click, to używając stopImmidatePropagation wywołają się tylko te, które znajdą się przed użyciem tej metody.

Zanim jednak zdecydujesz się na użycie jednej z tych dwóch metod, koniecznie zwróć uwagę, czy nie zepsuje to w jakiś sposób istniejących mechanik. Takim przykładem mogą być wszelkie skrypty odpowiedzialne za analitykę na Twojej stronie (zliczanie kliknięć, itp.). One również mogą korzystać z nasłuchiwaczy i delegacji zdarzeń (mechanizm ten opisałem poniżej).

Delegacja zdarzeń

Zachowanie zdarzeń w JavaScript umożliwia zastosowanie bardzo przydatnego wzorca, znanego jako delegacja zdarzeń (event delegation).

Załóżmy, że mamy do stworzenia tabelę w HTML, której każda komórka powinna reagować na kliknięcie i wywoływać jakąś funkcję. Pierwsze co przychodzi do głowy, to dodanie nasłuchiwacza do każdego elementu osobno, ale taka sytuacja jest idealna do zastosowania delegacji zdarzeń

Zamiast dodawać wiele osobnych nasłuchiwaczy (co może negatywnie wpłynąć na wydajność), możemy dodać jeden, który znajdzie się na rodzicu wszystkich komórek tabeli.

Na tym właśnie polega delegacja - nasłuchujemy na zdarzenie na nadrzędnym elemencie, dzięki czemu unikamy sytuacji, w których każdy element posiada osobny nasłuchiwacz. Znacznie łatwiej jest także usunąć nasłuchiwacz z jednego elementu, niż z wielu osobnych, więc zarządzanie nim jest również wygodniejsze.

Podsumowanie

Znajomość teorii dotyczącej działania eventów w JavaScript jest kluczowa, żeby w pełni świadomie korzystać z możliwości, które nam dają.

Z użyciem takich metod jak stopPropagation oraz stopImmidatePropagation możemy kontrolować propagację zdarzeń do kolejnych elementów HTML, a wykorzystując ostatni argument metody addEventListener (obiekt opcji), jesteśmy w stanie zmienić domyślne zachowanie eventu.

Warto w tym miejscu wspomnieć, że obiekt ten posiada jeszcze kilka właściwości, które mogą być przekazane, np. once (zdarzenie zostanie wywołane tylko raz), lub signal (przyjmuje AbortSignal, który pozwala na anulowanie zdarzenia).

Avatar: Wojciech Rygorowicz

Software Engineer / Fullstack developer

Wojciech Rygorowicz

wojciech.rygorowicz@gmail.com

Podziel się na

Dodaj komentarz

Komentarze (0)

Brak komentarzy