Często słyszę oburzenie przeciwników JavaScriptu, jakoby przez wieczne funkcje zwrotne (callbacks) JavaScript był prymitywnym i strasznym językiem narzucającym trudne do opanowania zależności i zagnieżdżenia kodu.
Owszem, JavaScript ma wiele słabych stron i można mu sporo zarzucić, jednak sterowanie poprzez zdarzenia jest piękną cechą tego języka.
Często można trafić na kod początkujących programistów JavaScript, przypominający ten poniższy:
1 2 3 4 5 6 7 8 9 10 11 12 13 | Lib('a').load(function() { Lib('b').load(function() { Lib('b').load(function() { form.submit(function() { submitViaAJAX(function() { alert('Wysłałem!'); }); }); }); }); }); |
Zagnieżdżenia i problemy z callbackami w JavaScript to normalka i nieraz spotkałem się z jeszcze gorszymi strukturami. Co gorsze, kilka lat temu i mi zdarzało się tak pisać.
Z powyższym kodem można sobie poradzić na kilka sposobów, a poniżej omówię jedno z moich ulubionych podejść – sterowanie przez zdarzenia.
Obługa zdarzeń w JavaScript
W swoich przykładach posłużę się biblioteką jQuery, ponieważ na niej głównie pracuję. Poza tym ma wbudowany prosty, acz rozbudowany mechanizm obsługi zdarzeń.
Interesującą nas metodą jest znane wszystkim on służące do przypinania funkcji reagujących na dane zdarzenie, np.
1 | jQuery(element).on('click', doSomething); |
czy delegując zdarzenia także do dynamicznie tworzonych elementów:
1 | jQuery(document).on('click', element, doSomething); |
Tak stworzone handlery możemy łatwo uruchomić w dowolnym momencie – wystarczy posłużyć się metodą trigger:
1 | jQuery(element).trigger('click'); |
To bardzo podstawowa obsługa zdarzeń znana przez wszystkich i stosowana na większości stron WWW. Niestety wielu programistów ogranicza się do tej podstawy, co jest grzechem w obliczu kolejnych możliwości.
Niestandardowe zdarzenia w JavaScript
Niestandardowe zdarzenia to bardzo przydatna funkcjonalność pozwalająca na lepszą organizację kodu, jak i usprawnienie sposobu reagowania na zdarzenia klienta.
Aby stworzyć niestandardowe zdarzenie wystarczy posłużyć się wyżej opisaną metodą on:
1 | jQuery(element).on('myCustomEvent', doSomething); |
by później je wywołać w dowolnym momencie:
1 | jQuery(element).trigger('myCustomEvent'); |
Domyślnie, przeglądarki mają wbudowanych wiele różnych zdarzeń zależnych od wykonywanych operacji. I na tym też powinniśmy się wzorować, budując swoją aplikację.
Podejście to ma wiele zalet, jak choćby:
- pozwala uniknąć zagnieżdżonych struktur kodu, lepiej organizuje kod,
- możesz w dowolnym momencie wywoływać dowolne zdarzenia na dowolnych elementach (mówiąc inaczej, masz ogromną uniwersalność),
- umożliwia tworzenie łatwych do rozszerzenia aplikacji przez możliwość dodawania wielu zdarzeń do pojedynczych elementów, jak również usuwanie czy nadpisywanie istniejących zdarzeń,
- możesz tworzyć własne przestrzenie nazw w zdarzeniach (np. dla wtyczki), co pozwala wykonywać tylko zdarzenia z określonej przestrzeni,
- nie musisz tworzyć osobnego, zewnętrznego API – Twoje zdarzenia będą globalne.
W praktyce/w oparciu o pseudokod, tak może wyglądać Twoja wtyczka:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | var Tabs = function(element) { this.element = jQuery(element); // some code }; Tabs.prototype.open = function() { // open tab this.element.trigger('open'); }; Tabs.prototype.close = function() { // close tab this.element.trigger('close'); }; jQuery.fn.tabs = function() { return this.each(function() { new Tabs(this); }); }; |
Tak przygotowaną wtyczkę łatwo użyć w praktyce:
1 | jQuery(element).tabs(); |
Równie łatwo dobrać się później do dowolnego elementu, reagując na określone zdarzenia:
1 | jQuery(elementA).on('open', doSomething); |
Zdarzenia w przestrzeni nazw
Kolejną przydatną funkcjonalnością przy sterowaniu aplikacją przez zdarzenia są przestrzenie nazw. Pozwalają one uniknąć wykonywania niepotrzebnych operacji, jak również porządkują kod czy grupują podobne operacje.
Przestrzenie nazw w zdarzeniach przydają się choćby w elementach, do których podpinamy wiele różnych funkcji nasłuchujących zdarzenie. Kiedy już podepniemy nasze zdarzenie z przestrzenią, możemy je wykonać, nie wykonując innych funkcji przypisanych do tego zdarzenia (spoza naszej przestrzeni).
Posłużmy się przykładem. Mamy jakiś element i chcemy w momencie jego kliknięcia wykonać niezliczone, zasobożerne operację :-)
1 | jQuery(element).on('click', doSomethingA); |
W powyższym kodzie dodaliśmy funkcję nasłuchującą doSomethingA, która się wykona w momencie kliknięcia w element.
Do tego samego elementu postanowiliśmy dodać kolejną funkcję nasłuchującą zdarzenie click:
1 | jQuery(element).on('click.myPlugin', doSomethingB); |
Zwróć uwagę, że po nazwie zdarzenia dodaliśmy kropkę i następnie nazwę naszej przestrzeni. Od tego momentu, kliknięcie w element (lub odpalenie zdarzenia przez metodę trigger) spowoduje wykonanie dwóch funkcji, doSomethingA oraz doSomethingB.
Co jeśli nie będziemy chcieli wykonywać wszystkich funkcji nasłuchujących zdarzenie, by nie obciążać przeglądarki niepotrzebnymi zadaniami? Wystarczy poniższe polecenie:
1 | jQuery(element).trigger('click.myPlugin'); |
Prosto i przyjemnie! Wywołując zdarzenie możemy również przekazać dodatkowe parametry:
1 | jQuery(element).trigger('click.myPlugin', [param1, param2]); |
Wady rozwiązania
Nie znam takowych :-)
Używam takiego podejścia od jakiegoś czasu i sprawdza się bardzo dobrze. Nie znaczy to jednak, że nie ma wad – jeśli ktoś zna, zapraszam do podzielenia się nimi w komentarzu.
Nie jest to jedyny sposób postępowania z funkcjami zwrotnymi w aplikacji. Być może w kolejnych wpisach omówię inne sposoby sterowania aplikacją (a jeszcze kilka fajnych się znajdzie).
Hmmm nie widzę nic złego w callbackach, ale w nadużywaniu funkcji anonimowych już tak.
Skoro już tytułujesz tekst jako JS, to warto zaznaczyć co jest obsługiwane w JS, a co jest wymysłem jQuery (namespace).
Dobrze by było dodać także informacje o preventDefault, stopPropagation i jak nimi kontrolować przepływ zdarzeń.
Trochę po macoszemu potraktowałeś sposób tworzenia zdarzeń, a wiedząc co i jak można przesyłać [i]event driven[/i] pokazuje co potrafi.
Nie tylko w JS :)
Wszystko fajnie, tylko że w przypadku zdarzeń i podejścia do czegoś takiego, można łatwo sprawić, że kod jest nieczytelny i niewiadomo, co z czego wynika.
Rozwiązaniem tego problemu jest coś, o czym nie mówi się w kursach, mianowicie Javascript Promises, w jQuery obecne jako Deferred.
Ale jaka jest różnica pomiędzy zwykłym wywołaniem funkcji (poza koniecznością użycia jQuery)? Może nie do końca zrozumiałem, ale nie widzę tutaj nic ponad dodatkową warstwę opakowującą funkcję :)
@Michał: Fakt, w tytule mogłem zastąpić JavaScript słowem jQuery. Ale z drugiej strony w czystym JavaScripcie też można napisać własny, prosty mechanizm obsługi niestandardowych zdarzeń, wraz z przestrzenią znaków i wszystkim innym, ale nie w tym był sens tego postu.
Mógłbym też przy okazji omówić bąbelkowanie zdarzeń, obiekt jQuery.Event, stworzyć własny mechanizm niestandardowych zdarzeń i mnóstwo innych, powiązanych ze sobą rzeczy. Tylko, że:
– długich wpisów nikt nie czyta, co najwyżej skanuje (widzę to po sobie),
– długie wpisy się długo pisze i później jeszcze dłużej podchodzi do napisania kolejnego (braku czasu i motywacji na pisanie takich kolosów).
Już jakiś czas temu postanowiłem zmienić nieco sposób prowadzenia bloga – mam wolną chwilę to siadam i piszę szybki artykuł omawiający jeden konkretny temat. Będzie kolejna chwila to omówię temat poboczny w osobnym wątku lub cokolwiek innego, czego chcę się nauczyć/utrwalić czy uznam, że warto się podzielić :-)
@eRIZ: Deferred nie jest mi obce i też używałem w praktyce – dobrze się sprawdza i jest to temat na osobny wątek prezentujący alternatywne podejście (jedno z wielu w sumie).
Cały JavaScript jest zdarzeniowy, więc tworzenie własnych zdarzeń jest jak najbardziej dobrym, naturalnym podejściem. Można sprawić, że kod będzie nieczytelny, jasne, ale to wszystko zależy od zastosowanej architektury (lub jej braku).
Ostatnimi czasy uczestniczę w tworzeniu dość sporych serwisów z takim podejściem i nie ma większych problemów, aplikacja jest bardzo łatwa do rozbudowy/modyfikacji. Ba, takie podejście też wykorzystują popularne ostatnio frameworki, jak choćby Backbone (+ narzucają fajną architekturę do tego).
@Kiro: patrz zalety, które można skontrastować z funkcyjnym podejściem.
@Kamil – bez przesady, całego mechanizmu nie musisz opisywać. Jednak preventDefault i stopPropagation ma znaczny wpływ – szczególnie gdy podpinasz się pod zdarzenie a ktoś wcześniej je killował i dziwisz się że nie działa.
@Michał: to musiałbym też od razu dorzucić opis pointer-events, by nie było odwrotnej sytuacji, np. masz element (A) z podpiętym zdarzeniem i na nim pozycjonujesz inny element (B), bez żadnego zdarzenia. Mimo to kliknięcie w element B wywołuje zdarzenie z elementu A, a ktoś może sobie tego nie życzyć :-)
We wpisie chciałem się skupić na jednym ze sposobów radzenia sobie z callbackami, zagnieżdżeniami i nieco lepszą organizacją kodu.