Praca z DOM prędzej czy później wymusi na każdym zapoznanie się z event listenerami. Ten prosty mechanizm pozwala nam reagować w momencie, gdy w aplikacji sieciowej coś się dzieje – użytkownik kliknie przycisk, jakaś animacja się zakończy, wczyta się zawartość ramki… Jednak czasami taki system zdarzeń przydałby się w logice naszej aplikacji. Wówczas w jednym miejscu moglibyśmy reagować na rzeczy, które dzieją się w innych częściach aplikacji. Na szczęście okazuje się, że w przeglądarce jest na to sposób.

Nasz event emitter

Żeby stworzyć prosty event emitter, wystarczy stworzyć obiekt klasy EventTarget:

const eventEmitter = new EventTarget();

Listenery przypina się do niego dokładnie tak samo jak do elementów DOM:

eventEmitter.addEventListener( 'hublabubla', console.log );

Jedynym udziwnieniem jest to, że… same eventy również tworzy się tak samo jak dla elementów DOM:

const event = new CustomEvent( 'hublabubla', { // 1
    detail: { // 2
    	some: 'info'
    }
} );

eventEmitter.dispatchEvent( event ); // 3

By stworzyć nowe zdarzenie, musimy się posłużyć klasą CustomEvent (1; można też zastosować każdy inny konstruktor zdarzeń, łącznie z generycznym Event). W drugim parametrze możemy podać obiekt z własnością detail (2), która zawierać może dodatkowe informacje, jakie chcemy przekazać do listenera. Żeby ostatecznie zdarzenie do listenera trafiło, trzeba użyć jego metody dispatchEvent() (3).

I choć nie jest to jakoś superdużo roboty, to jestem w stanie zrozumieć tych wszystkich zawiedzionych, którzy dojdą do wniosku, że to jednak nie jest najprostszy z najprostszych sposobów. Na szczęście, po klasie EventEmitter można dziedziczyć i stworzyć swój własny emiter z pomocniczą metodą fire():

class EventEmitter extends EventTarget {
    fire( name, detail ) { // 1
        const event = new CustomEvent( name, { // 2
            detail
        } );
        
        return this.dispatchEvent( event ); // 3
    }
}

Tworzymy w niej event na podstawie przekazanych danych – jego nazwy i własności detail (1). Dane te przekazujemy do konstruktora CustomEvent (2). Na samym końcu wywołujemy zdarzenie (3).

Wykorzystanie jest praktycznie takie samo jak “czystego” EventTarget:

const eventEmitter = new EventEmitter();

eventEmitter.fire( 'hublabubla', {
    some: 'info'
} );

Nie musimy jedynie martwić się o własnoręczne tworzenie CustomEventa, ponieważ tym zajmuje się nasza nowa metoda.

EventTarget – co to właściwie jest?

System zdarzeń był w DOM-ie niemal od samego początku. Jednak potrzeba było sposobu, aby w jakiś sposób ujednolicić go pomiędzy wszystkimi elementami DOM, BOM-em (Browser Object Model, czyli m.in. navigator i inne własności globalnego obiektu window) itd. Stąd powstał interfejs EventTarget. To właśnie w nim znajdują się metody takie jak addEventListener() czy dispatchEvent(). I ten interfejs następnie implementują praktycznie wszystkie rzeczy dostępne w JS-owym środowisku przeglądarki, które używają zdarzeń:

window instanceof EventTarget; // true
document instanceof EventTarget; // true
document.createElement( 'div' ) instanceof EventTarget; // true
new XMLHttpRequest() instanceof EventTarget; // true

Tradycyjnie interfejsy DOM-owe były tworami mocno abstrakcyjnymi. Jeśli były obecne w przeglądarce, to zazwyczaj można je było wykorzystać co najwyżej do sprawdzenia przy pomocy operatora instanceof, czy jakiś obiekt dany interfejs implementuje. Jednak pewien czas temu to się zmieniło i sporo interfejsów zamieniło się w de facto pełnoprawne klasy, które można instancjonować. Tak się też stało w przypadku EventTarget, dzięki czemu można tworzyć swoje własne event emittery bez potrzeby wykorzystywania jakiejkolwiek biblioteki zewnętrznej.

Haczyki

Oczywiście są haczyki, w tym wypadku dwa. Pierwszy polega na tym, że cały ten system zdarzeń jest w pełni synchroniczny, więc jeśli jakiś listener ma wykonać akcję asynchroniczną, to trzeba jednak sięgnąć po jakieś nienatywne rozwiązanie, np. emittery, lub napisać samemu.

Drugi haczyk polega na tym, że EventTarget to typowo przeglądarkowe API. To oznacza, że Deno je ma, ale Node… również je ma. Więc w sumie jest tylko jeden haczyk.

I to by było na tyle. Events on!