Prototype események

Ez a this nem az a this.

Mivel az "esemény" és annak kezelése a JavaScript-ben az egyik legfontosabb dolog, ezért nem teljesen mindegy, hogy mégis hogyan csináljuk. Vagy méginkább az, hogy hogyan kell csinálnunk. Értem itt ezalatt azt, hogy minden egyes alkalommal egy kisebb fajta regényt kell írnunk, amikor egy objektum egy eseményét akarjuk figyelni, vagy elég egy Event.observe() hívás, ami elvégez számunkra minden piszkos munkát.

Az "Objektum-orientált JavaScript" című bejegyzésem utolsó példájával kapcsolatban felmerült, hogy kicsit jobban is kifejthettem volna az ott történteket. Mivel ott nem az eseményeken volt a hangsúly, ezért gondoltam egy külön bejegyzésben teszem ezt meg.
A prototype.js az események téren is képes a dolgunkat a végtelenségig leegyszerűsíteni. Egészen addig, amíg mi magunk nem akarjuk a dolgokat "bonyolítani", és bele nem fogunk az objektum-orientáltságban. Ilyenkor kezdődnek a problémák. Tegyük fel, hogy szeretnénk egy olyasmi dolgot megoldani, hogy két gomb segítségével egy harmadik gombra tudunk eseményt tenni, majd azt az eseményt levenni a gombról. Objektumok nélkül nem lenne különösebben nehéz dolgunk:

function add_event(event)
{
    Event.observe('myButton_1', 'click', alert_me);
}
function remove_event(event)
{
    Event.stopObserving('myButton_1', 'click', alert_me);
}

var text = 'Ez a \'text\' tartalma';
function alert_me(event) {
    if (text != null) {
        alert('A \'text\' tartalma: '+text);
    }
    else {
        alert('A \'text\' nincs definiálva!');
    }
}

Event.observe(window, 'load', function(event) {
    Event.observe('myButton_2', 'click', add_event);
    Event.observe('myButton_3', 'click', remove_event);
});

A dolog persze kezd bonyolódni, ha nem csak egy, hanem mondjuk n darab ilyen gomb trióra kell ráhúznunk ezeket az eseményeket, így joggal merül fel az OO-ra az igény. Hirtelen felindulásból a fenti mintájára összedobunk valami ilyet:

var Teszt = Class.create();
Teszt.prototype = {
    initialize : function(alert_button, event_add_button, event_remove_button, text)
    {
        this.alert_object = $(alert_button);
        this.add_object = $(event_add_button);
        this.remove_object = $(event_remove_button);
        
        this.text = text;
        
        Event.observe(this.add_object, 'click', this.add_event);
        Event.observe(this.remove_object, 'click', this.remove_event);
    },
    
    add_event : function(event)
    {
        Event.observe(this.alert_object, 'click', this.alert_me);
    },
      remove_event : function(event)
    {
        Event.stopObserving(this.alert_object, 'click', this.alert_me);
    },
    
    alert_me : function(event)
    {
        if (this.text != null) {
            alert('A \'text\' tartalma: '+this.text);
        }
        else {
            alert('A \'text\' nincs definiálva!');
        }
    }
};

Event.observe(window, 'load', function(event) {
    var gombok = new Teszt('myButton_1', 'myButton_2', 'myButton_3', 'Ez a \'text\' tartalma');
});

Kódunk több sebből vérzik, gyakorlatilag a halálán van, és a hirtelen támadt pánikban lövésünk sincs, hogy mi a franc baja lehet ennek a látszólag működőképes kódnak. Miközben a prototype.js összes fejlesztőjének rokonságát végigszidjuk, lelünk rá a dokumentációban pár érdekes részletre, valami bind() függvénnyel kapcsolatban.
A probléma az, hogy amikor megnyomjuk az "Esemény hozzáadása" gombot, és lefut az add_event() függvény, az abban a függvényben lévő "this" az nem éppen az lesz, mint amire mi gondolunk. Ahhoz, hogy ez a "this" ugyanaz legyen, mint ami az initialize() függvényben a "this", pár változtatást kell eszközölnünk:

Event.observe(this.add_object, 'click', this.add_event.bind(this));
Event.observe(this.remove_object, 'click', this.remove_event.bind(this));

Ami azt illeti, a dolog működik, csak éppen az alert_me() folyamatosan azt mondja nekünk, hogy a text nincs megadva, pedig de. Mielőtt ismét elővennénk a fejlesztők rokonságát, látjuk, hogy talán az add_event() részbe se ártana egy bind(), hátha ugyanaz a probléma:

Event.observe(this.alert_object, 'click', this.alert_me.bind(this));

És csodák csodájára működik... izé... öhm... majdnem. Hozzáadjuk az eseményt az egyik gombbal, megnyomjuk a gombot, kiírja a megfelelő szöveget, majd levesszük az eseményt, megnyomjuk a gombot, és látjuk, hogy az esemény még mindig rajta van. Ennek az oka az, hogy az Event.stopObserving() segítségével csak elnevezett függvényeket tudunk a megfigyelésből kivonni. De hát ez el van nevezve, az a neve, hogy this.alert_me. Azaz, hogy majdnem az a neve. Mert a mi alert_me() függvényünkön meghívtuk a bind()-et, ami egy névtelen függvénnyel tér vissza, így szivatva meg minket.
Még mielőtt rátérnénk a megoldásra, nézzük meg mi is az a névtelen függvény, hogy biztosan teljesen képben legyünk:

Event.observe(window, 'load', function(event) {
    alert('Névtelen függvény vagyok!');
});

var myFunction = function(event) {
    alert('Én nem vagyok névtelen függvény!');
};
Event.observe(window, 'load', myFunction);

Tehát, az Event.stopObserving() csak a második esetben használható, amikor tudjuk annak a függvénynek a nevét, amit le akarunk állítani. Nincs más dolgunk, mint az initialize() végére fűzni egy sort...

this.alert_event = this.alert_me.bind(this);

...valamint a következőképpen módosítani az add_event() és remove_event() függvényeket:

add_event : function(event)
{
    Event.observe(this.alert_object, 'click', this.alert_event);
},
remove_event : function(event)
{
    Event.stopObserving(this.alert_object, 'click', this.alert_event);
},

A megoldás már csak annyiban kifogásolható, hogy a bind() helyett a prototype.js nyújt számunkra egy másik függvényt is, amit kifejezetten eseménykezelőkre lett kitalálva. Ez a gyönyörű nevű bindAsEventListener(). Miután lecseréltük az összes bind()-ünket erre a függvényre, készen is vagyunk. Egy - remélhetőleg - működő példa megtekinthető itt.

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha kedveled a régi jó dolgokat.