jQuery, zdarzenia - zastąpienie $.bind, $.delegate i $.live metodą $.on

Artykuł dodany: 10 listopada 2012. Ostatnia modyfikacja: 11 stycznia 2017.

Stopień trudności (1 - dla początkujących, 5 - dla ekspertów): 1

O jQuery – tytułem wstępu dla początkujących

jQuery to popularna biblioteka napisana w języku JavaScript mająca za zadanie uprościć i ujednolicić wiele czynności związanych z modyfikacjami drzewa DOM, animacjami, AJAXem oraz również obsługą zdarzeń. Powstała wiele lat temu jako odpowiedź na problemy web-developerów wynikające z różnic w nazewnictwie przeróżnych funkcji oraz działaniu w przeglądarce Internet Explorer oraz pozostałych, bardziej restrykcyjnie trzymających się zaleceń W3C. Jednak sam język ewoluuje, powstały nowe standardy, stosowane dawniej rozwiązania odchodzą do lamusa. Wykorzystywane dotychczas metody $.bind(), $.delegate() i $.live() znalazły swój nowoczesny odpowiednik $.on() – który postaram się w przejrzysty sposób opisać. Na potrzeby artykułu zakładam, że posiadasz już podstawową wiedzę o samym języku JS oraz stosowałeś wcześniej na swoich stronach, przynajmniej w bardzo podstawowym zakresie, bibliotekę jQuery.

Podstawowa konfiguracja projektu

Na początek stwórzmy bazową strukturę katalogową która będzie nam potrzebna do testowania kodu prezentowanego w artykule.

/htdocs
  /jquery-events
    /js
      jquery.min.js
      tut-events.js
    index.html

W swoim ulubionym edytorze utwórz plik index.html i przekopiuj poniższy kod:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8"/>
  <title>jQuery events</title>
  <!-- Biblioteka jQuery w wersji co najmniej 1.7 -->
  <script type="text/javascript" src="js/jquery.min.js"></script>
  <script type="text/javascript" src="js/tut-events.js"></script>
</head>
<body>
  <div id="test">
    <ul id="menu">
      <li><a href="http://wp.pl">Wp.pl</a></li>
      <li><a href="http://onet.pl">Onet.pl</a></li>
      <li><a href="http://interia.pl">Interia.pl</a></li>
    </ul>
  </div>
</body>
</html>

Ze strony jQuery pobierz zminimalizowaną wersję tejże biblioteki w wersji co najmniej 1.7 i przenieś ją do katalogu “jquery-events/js” jako plik jquery.min.js. Możesz również wykorzystać jedno ze zdalnych repozytoriów CDN. Wtedy zamiast linii:

<script type="text/javascript" src="jquery.min.js"></script>

wstaw

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>

Dodatkowo potrzebne będzie narzędzie do sprawdzania czy w kodzie nie wystąpił żaden błąd. Stosownie funkcji alert(‘Coś poszło źle!’) jest mało efektywne i nie przynosi niczego dobrego na dłuższą metę. W zależności od stosowanej przeglądarki narzędzia takie mogą być wbudowane (Opera Dragonfly, Chrome – narzędzia dla programistów) albo dla przeglądarki Firefox pobierz świetne rozszerzenie Firebug (uruchamiane klawiszem F12). Tak przygotowani możemy sprawdzić czy kod jQuery będzie się wykonywał:

tut-events.js

$(function() {
  //Wykonaj kiedy struktura DOM jest gotowa
  console.log('Hurra, działa');
  $('#menu li').css('background', '#D6D9E8');
});

W konsoli powinien wyświetlić się tekst “Hurra, działa” a wszystkie elementy listy uzyskają kolor “#D6D9E8”. Gdyby to nie nastąpiło sprawdź czy ścieżki do plików podane są prawidłowo oraz czy konsola nie zalogowała innych błędów. Tak przygotowani możemy przejść do zagadnienia właściwego.

Zdarzenia (Events)

Na typowej statycznej stronie występują dwa rodzaje akcji. Jedną z nich jest wysyłka formularza po kliknięciu w input type=submit, drugą podążanie za linkiem z kotwicy a href. Sytuacja może zmienić się diametralnie jeżeli na stronie zostanie osadzony JavaScript. Wtedy jesteśmy zdani na wyobraźnię autora skryptu. Próba wysłania formularza może być poprzedzona jego sprawdzeniem pod kątem poprawności wprowadzonych danych, kliknięcie w link może otworzyć nowe okno dialogowe albo wczytać dynamicznie treść do któregoś z elementów na stronie, wcześniej wykonując animację. Co więcej, akcji może być więcej niż tylko standardowe kliknięcie lewym przyciskiem myszy. Na stronie Mozilla Developer Network znajdziesz listę wszystkich obsługiwanych przez silnik Gecko (lub w pełnym dokumencie, dużo mniej przejrzystym, w specyfikacji W3C). Przytoczę kilka najczęściej stosowanych:

click – wciśnięcie przycisku myszy i zwolnienie nad danym elementem
dblclick – podwójne kliknięcie w dany element
mousemove – wskaźnik myszy został przesunięty nad element
keypress – wciśnięcia klawisza na klawiaturze
submit – formularz został wysłany
drag – element został przeciągnięty
drop – element został upuszczony

Do jednego elementu można przypisać wiele zdarzeń. Można również “powstrzymać” wykonanie zdarzenia domyślnego (czyli np przeniesienie po kliknięciu w kotwicę do nowej strony). Tutaj właśnie biblioteka jQuery ujednolica kod dla wszystkich przeglądarek.

Zastąpienie $.bind() i $.unbind()

Przez długie lata podstawową metodą stosowaną w jQuery do przypisywania/usuwania elementom zdarzeń były metody $.bind() i $.unbind(). Ich stosowanie wyglądało następująco:

tut-events.js

$(function() {
  //Wykonaj kiedy struktura DOM jest gotowa
  // Alternatywnie alias $('#menu li a').click(...);
  $('#menu li a').bind('click', function(event) {
    // Anuluj domyślną akcję, przekierowanie na stronę jednego z polskich portali
    event.preventDefault();
    // Wypisz w kosoli właściwości obiektu "event"
    console.log(event);
  });
});

Uruchom konsolę i przyjrzyj się bliżej obiektowi. Zobaczysz zrzut wszystkich właściwości klikniętego obiektu a. Sprawdzanie takie jest szalenie przydatne jeżeli nie wiesz bądź nie masz pewności dlaczego dany fragment kodu nie funkcjonuje prawidłowo albo gdy potrzebujesz dowiedzieć się co zostało zwrócone. Możesz dokonać niewielkiej modyfikacji kodu:

    // Wypisz w kosoli właściwości obiektu "event"
    console.log(event);
    console.log('Link powinien domyślnie przekierować do ' + event.target.href);

Zobaczmy jeszcze jak działa $.unbind():

$(function() {
  $('#menu li a').bind('click', function(event) {
    // Anuluj domyślną akcję, przekierowanie na stronę jednego z polskich portali
    event.preventDefault();
    console.log('Odnośnik kliknięty');
    // Usuń przypisane zdarzenie "click"
    $(this).unbind('click');
  });
});

Teraz sytuacja trochę się zmieniła. Pierwsze kliknięcie w kotwicę wypisze nam w konsoli “Odnośnik kliknięty”, jednak już drugie kliknięcie w ten sam link dokona przekierowania na adres właściwy podany w href. Wynika to stąd, że jQuery po zastosowaniu $.unbind() nie nasłuchuje dłużej czy wykonane zostało zdarzenie “click” na elmencie. Przy okazji warto w tym miejscu zauważyć że zapis taki jest jednoznaczny z zastosowaniem $.one():

$(function() {
  // Przypisz zdarzenie tylko raz do elementu
  $('#menu li a').one('click', function(event) {
    // Anuluj domyślną akcję, przekierowanie na stronę jednego z polskich portali
    event.preventDefault();
    console.log('Odnośnik kliknięty');
  });
});

Teraz gdy powinieneś lepiej rozumieć obsługę zdarzeń możemy już zastosować kod właściwy, metodę $.on():

$(function() {
  $('#menu li a').on('click', function(event) {
    event.preventDefault();
    console.log(event);
  });
});

Metoda $.bind(), w celu zachowania wstecznej kompatybilności biblioteki będzie dostępna jeszcze przez jakiś czas, jednak lepiej już teraz z niej zrezygnować. Jak widzisz zmieniliśmy tylko nazwę “bind” na “on” więc w podstawowym zakresie nasz kod będzie całkowicie zgodny.

Analogicznie, zamiast $.unbind() wystarczy zastosować $.off():

$(this).off('click');

Zastąpienie $.delegate() i $.undelegate(), delegacja zdarzeń

Przedstawiona wcześniej metoda $.bind() przypina do n występujących na stronie elementów odpowiednie zdarzenia, gdzie n to dowolna liczba całkowita. Rodzi to pewne problemy. Co jeżeli dodamy do strony elementy generowane dynamicznie a w dodatku nowych elementów będzie kilka tysięcy? Niech poniższy przykład zilustruje sytuację:

$(function() {
  // Każdy element "a" dostępny na stronie nasłuchuje zdarzenia "click"
  $('#menu li a').on('click', function(event) {
    event.preventDefault();
    console.log(event);
  });
  // Po czym utworzono nowy element listy
  $('#menu').append('<li><a href="http://gazeta.pl">Gazeta.pl</a></li>');
});

Co się stało? Pierwsze trzy pozycje listy istniały już w strukturze DOM kiedy dodawaliśmy im zdarzenie “click”, po kliknięciu wyświetlą nam w konsoli dane o obiekcie. Jednak nowo utworzona czwarta pozycja nic o tym nie wie, zachowa się jak każdy domyślny element czyli przekieruje zgodnie z działaniem typowych linków na stronie. Aby to poprawić musielibyśmy ponownie wywołać zdarzenie $.on() po dodaniu elementu. Przy większej ich liczbie wydajnościowy horror z dużymi wyciekami pamięci. Istnieje jednak pewien sposób aby temu zaradzić. Gdy wykonujemy jakąkolwiek akcję na elemencie jest on tym tak “podekscytowany” że natychmiast informuje o niej swoich rodziców, oni z kolei własnych i tak po kolei w drzewie DOM aż do pierwszego rodzica – document. Po angielsku cały proces nazywa się “event bubbling”. Możemy to teraz wykorzystać. Skoro rodzić wie co należy w określonych sytuacjach zrobić, może swoją wiedzę przekazać dzieciom – nawet tym nie narodzonym jeszcze. W jQuery odpowiada za to metoda $.delegate() – dostępna od wersji 1.4.2.

$(function() {
  // Rodzic "#menu" nasłuchuje zdarzenia "click" i informację o tym,
  //jak należy się zachować gdy wystąpi przekazuje swoim dzieciom "a"
  $('#menu').delegate('a', 'click', function(event) {
    event.preventDefault();
    console.log(event);
  });
  // Po czym utworzono nowy element listy
  $('#menu').append('<li><a href="http://gazeta.pl">Gazeta.pl</a></li>');
});

Ponieważ jednak mamy już metodę $.on() która ujednolica obsługę wszystkich zdarzeń wykorzystamy ją zamiast $.delegate():

$(function() {
  // W metodzie $.on składniowo zamieniamy parametr 'click' z 'a'
  $('#menu').on('click', 'a', function(event) {
    event.preventDefault();
    console.log(event);
  });
  // Po czym utworzono nowy element listy
  $('#menu').append('<li><a href="http://gazeta.pl">Gazeta.pl</a></li>');
});

Tym samym zdarzenie zostało przypięte tylko do jednego elementu, w dodatku zadziała na wszystkie nowo utworzone w przyszłości węzły. Aby jak najbardziej zwiększyć wydajność dobrze jest stosować jako rodzica najbliższy niezmienny element – w przykładzie #menu zamiast #test, a document tylko w ostateczności.

Analogicznie do $.unbind() istnieje również metoda $.undelegate():

$(function() {
  $('#menu').delegate('a', 'click', function(event) {
    event.preventDefault();
    console.log(event);
    // Zdejmij nasłuch dla wszystkich węzłów
    $('#menu').undelegate('a', 'click');
  });
  // Po czym utworzono nowy element listy
  $('#menu').append('<li><a href="http://gazeta.pl">Gazeta.pl</a></li>');
});

Której odpowiednikiem będzie:

$('#menu').off('click', 'a');

Czasami może wystąpić sytuacja w której dziecko nie chce, aby rodzić dowiedział się że jakieś zdarzenie miało miejsce. Aby przerwać propagację zdarzeń w górę drzewa należy wykorzystać metodę event.stopPropagation():

$(function() {

  $('#menu').on('click', 'li', function(event) {
    console.log('Kliknięte li');
  });

  $('#menu').on('click', 'a', function(event) {
    event.preventDefault();
    console.log('Link powinien domyślnie przekierować do ' + event.target.href);
    // Zakomentuj poniższą linię
    event.stopPropagation();
  });
  // Po czym utworzono nowy element listy
  $('#menu').append('<li><a href="http://gazeta.pl">Gazeta.pl</a></li>');
});

Zastąpienie $.live() i $.die()

Jeszcze przed wprowadzeniem delegacji zdarzeń za pomocą $.delegate(), w jQuery 1.3 pojawiła się opcja $.live(). Finalnie efekt stosowania był niemal identyczny (nie licząc pewnych błędów) jednak samo stosowanie obwarowane było dużym narzutem wydajnościowym. Aktualnie pod żadnym pozorem nie powinno się z tej metody korzystać. Nie działa w niej stopPropagation(), nie działa albo działa niepoprawnie łączenie metod (np. $(“a”).find(“.offsite, .external”).live( … ); ) i kilka innych wymienionych w dokumentacji jQuery. W wersji 1.9 jQuery metoda live() została całkowicie usunięta. Analogicznie nie należy stosować metody $.die().

Zdarzenia a przestrzenie nazw

Ciekawym dodatkiem który jest dostępny w metodach $.on() i $.off() jest również przestrzeń nazw (namespace). Do każdego zdarzenia możemy przypisać uniwersalny identyfikator (podobnie jak klasy w CSS) który następnie mamy możliwość wywołania lub usunięcia pojedynczo, zostawiając pozostałe powiązane z elementem zdarzenia. Przykładowo:

$(function() {

  $('#test').css({
    'display':'block',
    'width': '150px',
    'height': '50px',
    'background': 'green'
  }).html('Przykładowy tekst');

  $('#menu').css('display', 'none');

  $('#test').on('click.font', function(event) {
    console.log('Zdarzenie click.font');
    $(this).css('font-size', '20px');
  });

  $('#test').on('click.background', function(event) {
    console.log('Zdarzenie click.background');
    $(this).css('background', 'yellow');
  });
});

Kliknięcie na zielony box #test wywoła dwa zdarzenia: ‘click.font’ oraz ‘click.background’. Możemy teraz zrezygnować ze zdarzenia ‘click.background’ pozostawiając wszystkie inne:

$('#test').off('click.background');

Obserwuj w konsoli jak zmienia się logowanie wywołania zdarzeń. Przy większych projektach przestrzenie nazw są nieocenione.

Podsumowanie

Dzięki nowej wersji jQuery zyskaliśmy całkiem nową jakość w obsłudze zdarzeń. Ustandaryzowane API sprawia, że zamiast nauki i pamiętania 6 metod pozostają nam 2, które w dodatku obsługują wszystkie potrzebne akcje. Z całą pewnością warto już teraz wykorzystać metody $.on() i $.off() gdyż nie wiadomo jak długo twórcy jQuery zdecydują się zachować wsteczną kompatybilność. Na koniec wkleję jeszcze opis obu metod:

$.on( events [, selector] [, data] , handler(eventObject) ) //zwraca obiekt jQuery
$.off( events [, selector] [, handler(eventObject)] ) //zwraca obiekt jQuery

events – jedno lub więcej zdarzeń oddzielonych spacją – “click”, “focus”, “keypress” itd. Może zawierać dodatkowo przestrzenie nazw np “click.foo.bar”
selector – string służący do odfiltrowania potomka elementu wywołującego zdarzenie
data – dodatkowe dane przekazywane do funkcji “handler” w chwili wystąpienia zdarzenia
handler(eventObject) – funkcja wywoływana gdy nastąpi zdarzenie.

Zapraszam do przeczytania pełnej dokumentacji na stronie projektu.

Komentarze

Nie ma jeszcze żadnych komentarzy do wyświetlenia. Może chcesz zostać pierwszą osobą która podzieli się swoją opinią?

Dodaj komentarz

*
Nazwa zostanie wyświetlona wraz z komentarzem. Możesz też utworzyć nowe konto w serwisie, dzięki czemu uzyskasz dodatkową funkcjonalność.
*
Akceptowana jest ograniczona składnia Textile. Wszystkie tagi HTML zostaną usunięte.