Przypisywanie i uruchamianie eventów
Najprostszą metodą tworzenia zdarzeń w obrębie strony internetowej jest przypisywanie akcji (funkcji) bezpośrednio do odpowiednich elementów strony, np.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//JavaScript var element = document.getElementById('przycisk'); element.onclick = function() { console.log('kliknięto'); } //JQuery $('#przycisk').click(function(event){ console.log('kliknięto'); }); $('#przycisk').bind('click', function(event){ console.log('kliknięto'); }); |
Każdy z powyższych przykładów robi dokładnie to samo (aczkolwiek nie tak samo) – nadaje event, w wyniku którego po naciśnięciu elementu #przycisk w konsoli pokaże się napis kliknięto. Tym prostym sposobem można przypisać zdarzenia wszystkim elementom, które powinny zmieniać stan strony. To rozwiązanie nie rodzi trudności, kiedy elementy posiadające zdarzenie są rozłączne:
1 2 |
<div class="przycisk-1"><div> <div class="przycisk-2"></div> |
natomiast kiedy jeden zawiera się w drugim, jak przedstawiono poniżej
1 2 3 |
<div class="przycisk-1"> <div class="przycisk-2"></div> </div> |
rodzi się pytanie: Które zdarzenie powinno uruchomić się najpierw w przypadku kliknięcia myszką na elemencie .przycisk-2? Czy najpierw zdarzenie dla .przycisk-1 a później dla .przycisk-2 czy też odwrotnie?
W latach 90. twórcy konkurujących ze sobą przeglądarek (Netscape i IE) doszli do dwóch, całkowicie innych wniosków, w wyniku czego powstały dwa podejścia: przechwytywanie i bąbelkowanie (ang. odpowiednio: capturing, bubbling). Podejście Netscape było następujące: najpierw odpalamy zdarzenie na elemencie zawierającym ( .przycisk-1 ) a później na elemencie zawieranym ( .przycisk-2 ) – przechwytywanie. Podejście Microsoftu było odwrotne: najpierw odpala się event elementu zawieranego ( .przycisk-2), a dopiero potem elementu zawierającego ( .przycisk-1 ) – bąbelkowanie. Oba schematycznie zobrazowano na widoku 3D obu przycisków; poniżej.
Wcześniejsze wersje IE (do 8 włącznie nie posiadały opcji przechwytywania) pozostałe przeglądarki natomiast implementowały oba podejścia. Organizacja W3C w kwestii obydwu rozwiązań postanowiła zająć stanowisko pośrodku i w specyfikacji (z 2000 roku) model zdarzeń przewiduje najpierw fazę przechwytywania a następnie fazę bąbelkowania. Należy tu też zwrócić uwagę, że w przypadku przypisywań typu el.onclick = function(){} domyślnie używane jest bąbelkowanie. Podobnie sprawa ma się z JQuery, za pomocą którego możemy korzystać tylko z bąbelkowania.
Bąbelkowanie
W dalszej części to opracowanie skupia się tylko na JQuery i na następującym wycinku drzewa DOM:
1 2 3 |
<div class="przycisk-1"> <div class="przycisk-2"></div> </div> |
Jeśli do tych elementów dodamy następujące zdarzenia:
1 2 3 4 5 6 7 |
$('.przycisk-1').click(function(event) { console.log('Klik na 1'); }); $('.przycisk-2').click(function(event) { console.log('Klik na 2'); }); |
I klikniemy myszką w obrębie elementu .przycisk-2 , to w konsoli zobaczymy następujące wpisy:
1 2 |
Klik na 2 Klik na 1 |
Jeżeli bąbelkowanie nie jest pożądane, to można wywołać metodę stopPropagation() na obiekcie event przekazanym do funkcji. Bezpiecznie jest zrobić to na samym początku funkcji. Zmieniamy zdarzenie .przycisku-2 :
1 2 3 4 |
$('.przycisk-2').click(function(event) { event.stopPropagation(); console.log('Klik na 2'); }); |
i teraz w konsoli po kliknięciu na .przycisk-2 pojawi się tylko:
1 |
Klik na 2 |
Uwaga! Podobny efekt uzyskamy jeżeli funkcja zwróci false ( return false;), ale tak robić nie należy, ponieważ oprócz zatrzymania bąbelkowania do skutku nie dojdzie domyślna akcja elementu (np. wysłanie formularza). Może to też przeszkodzić przy delegowaniu (o tym dalej). Jeżeli chcemy zatrzymać bąbelkowanie i dodatkowo zapobiec odpaleniu domyślnej akcji, to najlepiej jawnie użyć do tego pary metod stopPropagation() i preventDetault().
Krótka historia metod odpalania eventów w JQuery
W pierwszej wersji biblioteki event można było dołączyć za pomocą metody bind() albo też aliasów takich jak click(). Praktyka pokazała, że tak nadawane zdarzenia w przypadku stron bardziej rozbudowanych były niewygodne. Np. jeżeli za pomocą AJAXu strona doczytywała fragment drzewa DOM, to należało na nowo odpalić wszystkie przypisania eventów dla elementów, które zostały dynamicznie dodane. Pojawił się plugin dodający metodę live() – metoda działała analogicznie do bind() , ale w przypadku dynamicznie dodanych elementów nie trzeba jej było ponownie odpalać. Po pewnym czasie metoda ta stała się częścią JQuery. W czasie jej pojawienia się w bibliotece dodano jednocześnie delegate() , która pozwalała delegować eventy (o tym dalej) i uzyskiwać efekty podobne do live() . Nadeszła wersja 1.7. live() zostało oznaczone jako deprecated (całkowicie zniknęło w 1.9), a pojawiła się metoda on() . Pozwala ona tak samo jak bind() przypisać zdarzenie do elementu, ale poza tym w pewnym układzie argumentów umożliwia delegowanie. Ta historia ma na celu zobrazowanie przemian w JQuery, a nie etapy wymyślania delegacji, bo ta nie wymaga wymienionej biblioteki.
Delegacja
Bąbelkowanie umożliwiło delegowanie zdarzeń. Event (np. click) posiada informację (odwołanie) o elemencie, „w którym został stworzony”. Wspomniana referencja jest dostępna za pomocą właściwości target (w IE do wersji 8 włącznie właściwość ta kryła się pod nazwą srcElement). Dzięki tej właściwości i dzięki currentTarget możliwe stało się przypisanie zdarzenia np. do .przycisku-1 i uruchomienie go w momencie kliknięcia na .przycisk-2 . Taki właśnie proces nazywamy delegacją. Ogólnie delegacja (delegowanie) to uruchomienie zdarzenia na elemencie wewnętrznym, którego obsługa jest wykonywana dopiero przy rozpatrywaniu tego zdarzenia w elemencie zewnętrznym.
Posłużmy się wcześniejszym fragmentem DOM:
1 2 3 |
<div class="przycisk-1"> <div class="przycisk-2"></div> </div> |
i przeanalizujmy wykorzystanie delegacji, za pomocą metody on() . Skorzystamy z wywołania do delegowania, czyli następującego wzorca $('selektorRodzica').on('zdarzenie', 'selektorPotomka', funkcja); :
1 2 3 |
$('.przycisk-1').on('click', '.przycisk-2', function(event) { console.log('Klik na 2'); }); |
Zgodnie z założeniami, klikamy na element wewnętrzny, a zdarzenie rozpatrywane jest na poziomie elementu zewnętrznego. Kliknięcie na .przycisk-1 nie wywołuje żadnej akcji. Analogiczny efekt uzyskalibyśmy przypisując zdarzenie do document:
1 2 3 |
$(document).on('click', '.przycisk-2', function(event) { console.log('Klik na 2'); }); |
Użycie return false; jak wspomniano wcześniej przerywa bąbelkowanie, a co za tym idzie potrafi przerwać też delegację, zapobiegając wykonaniu określonych akcji. Znalezienie błędu może być skomplikowane, dlatego lepiej przerywanie łańcucha zdarzeń robić bardziej jawnie z wykorzystaniem wspomnianych wyżej metod.
W przypadku skorzystania z delegacji i bezpośredniego przypisania zdarzenia, najpierw wykona się to drugie.
Należy nadmienić, że w przypadku delegacji należy trochę inaczej definiować funkcje obsługujące – np. jeżeli każdy element wewnętrzny ma po kliknięciu pokazywać alert z kolejną liczbą to, w przypadku zwykłego definiowania eventów, można w pętli przypisać dla każdego elementu odpowiednie zdarzenie. Natomiast jeżeli delegujemy, to tworzymy jedno zdarzenie w którym można by wykorzystać np. atrybut data-number elementu, który przechowywałby liczbę:
1 2 3 4 5 |
<div> <p data-number="10">lorem</p> <p data-number="20">lorem</p> ... </div> |
1 2 3 4 |
$(document).on('click', 'p', function(event) { var num = $(this).data('number'); alert(num); }); |
Korzystając z delegowania można uprościć zarządzanie zdarzeniami. Przyjmijmy, że na wykonywanej stronie należy oprogramować przycisk, który może być aktywny lub nie. Przycisk jest aktywny, jeżeli posiada klasę active. Bez użycia delegowania można w funkcji obsługującej zdarzenie sprawdzać czy element posiada klasę i w zależności od tego wykonywać odpowiedni kod ALBO używać metod bind() i unbind() by w odpowiednich momentach dodawać i usuwać obsługę zdarzeń. W przypadku delegowania można wykonywać odpowiednie instrukcje osobnymi zdarzeniami np.:
1 2 3 4 5 6 7 |
$(document).on('click', 'button.active', function(event) { // jakieś działanie }); $(document).on('click', 'button:not(.active)', function(event) { // inne działanie }); |
Powyższy przykład nie wydaje się być dużo bardziej korzystny, ale jeżeli działanie ma zależeć od kilku elementów, które są wyżej w hierarchii, to powyższy sposób definiowania zdarzeń będzie przejrzystszy i prostszy do zmiany. Może posłużmy się przykładem następującym: po pierwsze wyobraźmy sobie, że przedmioty można opisać jak tagi HTML, a ich stan jako klasy. Pomińmy kwestię tego co zmienia stan. Załóżmy, że musimy oprogramować chowany dach kabrioletu w zależności od pogody, korzystając z delegowania można by zdarzenia zdefiniować następująco:
1 2 3 4 5 6 7 8 9 10 11 |
$(świat).on('uruchomienieSilnika', 'pogoda.słoneczna temperatura.powyzej20stopniC kabriolet', function() { // schowaj dach kabrioletu }); $(świat).on('uruchomienieSilnika', 'pogoda.słoneczna temperatura.minus10stopniC kabriolet', function(event) { // rozstaw dach kabrioletu }); $(świat).on('uruchomienieSilnika', 'pogoda.deszczowa kabriolet', function(event) { // rozstaw dach kabrioletu }); |
Zalety delegacji:
- Dla wielu potomków tworzymy jedną obsługę zdarzenia zamiast tworzyć ich wiele. Przechowywanie jednej obsługi zdarzenia jest zdecydowanie lepsze niż np. 10 tysięcy identycznych funkcji obsługujących.
- Nie trzeba martwić się o obsługę zdarzeń elementów dodawanych dynamicznie.
- Łatwiejsze zarządzanie zdarzeniami, dla określonych stanów struktury DOM – zdecydowanie prostsze w przypadku wielu zależnych elementów o różnych stanach.
Wady delegowania:
- Duża liczba zdarzeń zdefiniowana na document może spowolnić działanie strony, bo każde (lub większość) zdarzenie będzie testowane – czy nie powinna się uruchomić przypisana do niego funkcja. Można się przed tym co prawda uchronić korzystając z definiowania delegacji na bliższych rodzicach elementów, ale stworzone dynamicznie elementy będą wymagały dodatkowego przypisania eventów.
Podsumowanie
Korzystanie z delegowania niesie ze sobą wiele korzyści zarówno pod względem zużycia pamięci przeglądarki, uproszczenia obsługi stron dynamicznych, jak i zwiększenia przejrzystości kodu. To i niewielka liczba wad są dobrą rekomendacją do stosowania delegacji do oprogramowywania stron internetowych.