jQuery a prehnaná delegácia udalostí

3. března 2011

Keď jQuery pridalo do jadra funkciu .live, programátori ju začali používať bez toho, aby si zistili, o koľko dokáže nadbytočný live spomaliť stránku.

Ešte než live funkcia v jQuery vznikla, začal fungovať tzv. liveQuery plugin. Jeho použitie je obdobné ako použitie funkcií live v jQuery, no liveQuery môže aj vďaka nepochopeniu jeho funkčnosti byť naozajstnou záťažou pre kód. A to ešte väčšou ako pri použití live.

Pokúsim sa tu teda „polopate“ vysvetliť rozdiel medzi rôznymi technikami delegovania udalostí a ich dopad na kód, keďže som aj sám vďaka nesprávnemu použitiu liveQuery vyrobil z jednej nádhernej administratívnej aplikácie doslova mamuta…

Ako funguje .live()

Ako som už písal – mnoho vývojárov začalo po zavedení live funkcie túto veselo doslova zneužívať. Samozrejme bez toho, aby o tom vedeli. Ide totiž o to ako takáto delegácia udalostí funguje. Vezmime si príklad. Máme tabuľku na jednej jedinej stránke, pričom stránok máme dokopy 200. V tejto tabuľke chceme zaistiť, aby všetky odkazy po kliknutí vypísal výstražné okno. V tomto prípade je použitie live tak trochu prehnané.

$('#tabulka td a').live('click', function() {
    alert('Nejaky text.');
    return false;
});

Upravené: vďaka užívateľovi karf a jeho komentáru bol tento odstavec opravený (funkcia live naozaj nekontroluje zmenu DOM štruktúry, ale vykonávanie udalostí na stránke)

Dôvodom je spôsob akým live funguje. Táto funkcia totiž prilepí každú jednu delegáciu, ktorú vytvoríme k objektu document. Pretože live berie delegáciu tak, že sa náš element môže vyskytnúť kdekoľvek v dokumente, musí pri každej udalosti na dokumente preveriť, či sa táto netýka tabuľky s ID „tabulka“ a odkazu v nej. Ak pomocou nami daného selektoru jQuery zhodnotí, že sa ho to týka, vykoná funkciu, ktorú sme tejto udalosti priradili. V našom prípade pri kliknutí na odkaz vypíše výstražné okno.

Problém je, že ak máme našu tabuľku „tabulka“ len na jednej stránke z 200, ale na každej z nich používame ten istý kód, bude jQuery skúšať nájsť túto tabuľku na každej jednej z týchto 200 stránok – a to úplne zbytočne. Keďže selektor používa na vyhľadanie elementu v tomto prípade ID a je to jediná delegácia, ktorú na stránkach máme, zaťaženie nebude vôbec rozpoznateľné. Problém vzniká, ak by sme live použili týmto spôsobom napríklad 100 krát. To by sa už jQuery pekne zapotila, snažiac sa vyhľadať všetkých 100 selektorov pri každom jednom kliknutí.

A o čo horšie je takéto použitie so selektormi typu „a.special“! V tomto prípade musí jQuery prejsť všetky odkazy na našej stránke, nájsť tie, ktoré majú triedu „.special“. Ak by sme mali čo len 50 takýchto selektorov, takúto záťaž pri profilovaní už celkom isto rozpoznáme.

Na pomoc prichádza .delegate()

Skutočnosť, že je live funkcia v jQuery často zneužívaná, prípadne nahrádzaná jej špeciálnym manuálnym volaním zrejme inšpirovala vývojárov pridať do verzie 1.4.2 funkciu delegate. Rozdiel medzi týmito dvomi funkciami je, že zatiaľ čo live obsluhuje každú našu delegáciu až v objekte document, funkcia delegate obslúži delegáciu na elemente, ktorý sami zvolíme. Napríklad…

$('#tabulka').delegate('td a', 'click', function() {
    alert('Nejaky text.');
    return false;
});

Syntax je veľmi podobná predošlému príkladu, avšak dejú sa tu dve zásadne odlišné veci:

  • obsluha onClick udalosti je priradená k elementu s ID „tabulka“ – teda k našej tabuľke. To znamená, že element, ktorý „počúva“ kam sme klikli je tabuľka samotná. Ak klikneme mimo tabuľky, nebude pre jQuery potrebné – tak ako v prvom prípade – zisťovať, či naše kliknutie prišlo z tabuľku „tabulka“ a z jej odkazov, alebo z iného miesta v dokumente. To je jasná nevýhoda live, keďže musí pri každom kliknutí zisťovať kde na stránke sa vlastne kliklo – a to použitím celého selektoru („#tabulka td a“). Naproti tomu, delegate zisťuje kam sa kliklo len v rámci elementu, ktorý mu zadáme – v našom prípade len ak sa klikne na tabuľku s ID „tabulka“.
  • v prípade, že tabuľka s ID „tabulka“ z našeho dokumentu zmizne, odstráni sa aj delegácia všetkých onClick udalostí priradená ku odkazom v tejto tabuľke. Výhoda takéhoto riešenia je, že v prípade odstránenia elementu zo stránky sa automaticky vyčistí jedna obsluha udalosti (event handler) a uvoľní sa pamäť potrebná na uchovanie tejto obsluhy. Pri live toto zostáva v pamäti nastálo, pretože document, ktorý uchováva všetky delegácie, zo stránky nikdy nezmizne. Nevýhoda riešenia je, že ak sa tabuľka na stránka objaví znovu, musíme priradiť delegáciu znova – a ručne.

Rozdiel by mal byť už teraz jasný – delegate nám šetrí pamäť a rýchlosť, akou stránka reaguje. Samozrejme sme týmto riešením ešte neodstránili náš prvotný problém – čo ak je tá tabuľka len na jednej z 200 stránok? Síce ušetríme pamäť, ale stále tú máme selektor, ktorý musí bežať na každej stránke a na 199 z nich sa následne zahodí, pretože tam tabuľka jednoducho neexistuje. Riešenie je v tomto prípade ukrutne jednoduché – vkladajte script, ktorý obsluhuje odkazy v tabuľke len na stránku, kde je táto tabuľka prítomná :-)

Keď stačí bind, stačí bind

V našom prípade použitie delegácie naozaj ušetrí rýchlosť a pamäť. Avšak je mnoho prípadov, kedy leniví vývojári zneužijú live k tomu, aby priradili onClick udalosť napríklad jedinému jednému odkazu, ktorý sa „občas môže vyskytnúť na stránke“.

$('a.special').live('click', function() {
    alert('Tak som sa vyskytol a vy ste na mna klikli...');
    return false;
});

Namiesto jasného ošetrenia prípadov, kedy sa odkaz „a.special“ na stránku naozaj pridá, sa tu programátor spolieha na to, že keď sa tam pridá, bude mať automaticky priradenú onClick udalosť. Je to samozrejme pravda, no rovnako nám to zaručuje že:

  • sa vyskytne minimálne jedna neštandardná situácia, kde bude použitie priradenej funkcie úplne nesprávne
  • náš kód bude o ďalší selektor pomalší a náš (už tak masívny a doplnkami nabúchaný) Firefox vezme operačnému systému o jednu – väčšinu času zbytočnú – fukciu viac

Toto je praktika, ktorú vrele nedoporučujem, a skôr vyzývam programátorov na kontrolu a testovanie svojho kódu. Správne použitá obyčajná bind funkcia znamená napríklad aj to, že kódu, ktorý programujeme aj rozumieme…

$('#tabulka thead th a.special').bind('click', function() {
    alert('Som v hlavicke tabulky s identifikatorom "tabulka"...');
    return false;
});

Na čo nevedomých láka .liveQuery()

Tento skvelý plug-in, ktorý už pred zavedením live do jQuery ponúkal delegáciu udalostí, ponúka aj jednu „vymoženosť“, ktorá ak sa nepoužíva s rozvahou – môže narobiť viac zlého ako dobrého. Je to práve táto funkcionalita, ktorá urobila z môjho krásneho projektu liveQuery mamuta, žerúceho v niektorých prípadoch až 63 000 volaní rôznych funkcií, ktoré vo výsledku dajú stránku po ubehnutí 900ms (čiže takmer jednej sekundy). A to už po optimalizácii selektorov (predtým to boli cca. 2 sekundy a o niečo viac volaní). liveQuery totiž povoľuje programátorovi použiť syntax, ktorá síce nepridáva k elementom žiadne udalosti, napriek tomu si však môže vyžiadať slušné množstvo procesoru a pamäte:

$('p').liveQuery(function() {
    alert('Tak som niekde na stranke, co so mnou chcete robit...');
    return false;
});

Schválne používam nepríjemne globálny selektor, aby som ukázal asi to najhoršie čo sa môže programátorovi použitím liveQuery plug-inu podariť. V tomto prípade liveQuery jednoducho pri každej zmene v dokumente zisťuje – podobne ako live – či sa v ňom nevyskytol daný element. V tomto ohľade funguje približne rovnako, až na to, že s elementom vôbec nič nerobí. Delegácie a inú funkčnosť elementu necháva celkom na nás, čo sa môže pri neopatrných zásahoch niekedy hodne predražiť – s ohľadom na rýchlosť a využitie pamäte.

Nemôžem tvrdiť, že presne viem ako liveQuery funguje vo svojich útrobách, pretože som ich nikdy nevidel a ani nemal čas študovať. Ale pokiaľ sa jedná o funkcionalitu, ktorá jednoducho zisťuje či je daný element na stránke, viem si predstaviť, že pri tak vysokom počte selektorov aké obsahuje moja mega-aplikácia (v mojom prípade dokonca duplicitných tam, kde sa dalo použiť delegate, fuj, fuj) to bude chvíľku trvať.

A znova to znamená, že namiesto špecifických volaní pre rôzne udalosti nahádžeme všetko do jedného volania, ktoré to urobí za nás – v prípade, že daný element vôbec na stránke existuje… Toto teda znovu tak trochu implikuje, že nepoznáme všetky časti kódu tak, ako by sme potrebovali (ja viem, niekedy to inak nejde :-) Takže pozor na liveQuery a nadbytočné používanie tejto funkcionality. Čím viac selektorov, tým dlhšie to prehliadaču trvá a tým viac pamäte zaberie.

Chcem podotknúť, že týmto liveQuery v žiadnom prípade neodsudzujem. Tento text berte hlavne ako priateľské varovanie ;-)

Zdroje

Štítky: Články

Mohlo by vás také zajímat

Nejnovější

3 komentářů

  1. karf

    Bře 3, 2011 v 10:59

    Obávám se, že Váš popis funkce live je nesprávný. Správně píšete, že se událost live deleguje na document, ale pak do toho motáte zjišťování existence po změně DOM. Tohle se, pokud vím, vůbec neděje, proč by taky mělo? Prostě se na dokumentu při výskytu události matchuje target se selektorem. Na druhou stranu plugin liveQuery funguje (nebo aspoň dřív na začátu fungoval, nevím, jak současné verze) tak jak popisujete – sledováním změn v DOM a bindováním událostí k jednotlivým elementům.

    Odpovědět
  2. Martin Ambruš

    Bře 3, 2011 v 11:42

    vďaka za komentár, máte samozrejme pravdu, už je to opravené :-)

    Odpovědět
  3. MCZ

    Bře 4, 2011 v 13:59

    Jen čistě pro zajímavost, delegate volá toto

    delegate: function( selector, types, data, fn ) {
    return this.live( types, data, fn, selector );
    },

    Jinými slovy – i live lze používat stejným způsobem, čtvrtý parametr jen není uváděn v dokumentaci a zřejmě kvůli zachování zpětné kompatibility a pravidla, že selector bývá první parametr, raději tvůrci vytvořili novou „shorthand metodu.

    Odpovědět

Napsat komentář: MCZ Zrušit odpověď na komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *