Ten wpis częściowo porusza tematykę wzorców projektowych. Jeśli interesuje Cię tematyka wzorców projektowych, to zachęcam Cię do zapoznania się z artykułem, gdzie opisuję czym są wzorce projektowe. Znajdziesz tam też kompletną listę wzorców opisanych na moim blogu.
Jeśli masz jakieś, choćby szczątkowe, doświadczenie z JavaScriptem, to zapewne zdarzyło Ci się skorzystać z listenerów. Listenery nasłuchują na żądane zdarzenie lub zdarzenia, a po ich wystąpieniu wykonują jakąś zdefiniowaną akcję.
// HTML
<button id="foo">Foo</button>
// JavaScript
document.getElementById('foo').addEventListener( 'click', function() {
console.log('You clicked me!');
} );
Mając taki kod, za każdym razem, gdy zostanie kliknięty przycisk z id
równym foo, zostanie wykonany kod przekazany w callbacku. Oczywiście możemy nasłuchiwać nie tylko na kliknięcia, ale i na sporo innych zdarzeń. Pełną ich listę znajdziesz w materiałach dodatkowych, gdyż nie na tym skupia się ten artykuł.
Na bardzo podobnej zasadzie działają Web API dostarczane przez przeglądarki zwane observerami. Zanim jednak przejdę do ich omówienia, będzie potrzebne nieco teorii. Jak widzisz cały czas kręcimy się wokół obserwowania, czy też nasłuchiwania czegoś. Taka koncepcja w świecie programowania została opisana i zdefiniowana jako wzorzec projektowy obserwator.
O obserwatorze słów kilka
Jak sama nazwa wskazuje, głównym elementem tego wzorca jest obserwator (observer) oraz obserwowany lub obserwowani (observable, observee). Może to brzmieć jak masło maślane, ale najprościej mówiąc: obserwator obserwuje obserwowanych i wykonuje pewną zadeklarowaną akcję w momencie wystąpienia konkretnych warunków. Zobrazujmy to przykładem:
Załóżmy, że na naszym osiedlu mieszka złośliwy sąsiad, który bardzo nie lubi, gdy przy pobliskim trzepaku dzieci grają w piłkę. Sąsiad jest obserwatorem, natomiast trzepak jest obserwowanym. W chwili, gdy dzieci zaczynają przy nim grać w piłkę, obserwujący zauważa interesującą go zmianę w obiekcie obserwowanym i wykonuje akcję. W tym wypadku może to być próba przegonienia dzieci lub telefon na policję. Zależy jak bardzo złośliwy jest sąsiad 😉
Przykłady zastosowania w życiu codziennym można mnożyć w nieskończoność. Innym przykładem z życia może być przesłanie powiadomienia o nowym filmie w serwisie YouTube. Użytkownik tworzy obserwatora, który obserwuje dany kanał, który staje się obserwowanym. Obserwujący wyśle zainteresowanemu użytkownikowi powiadomienie, czyli wykona zadeklarowaną akcję w momencie wystąpienia interesującego nas zdarzenia, czyli publikacja nowego filmu. Podobny mechanizm wysyłania notyfikacji możemy spotkać praktycznie w każdej aplikacji mobilnej.
Struktura komponentów składających się na implementację wzorca obserwator w klasycznym podejściu prezentuje poniższy diagram.
Subject reprezentuje podmiot, który jest obserwowany. Observer natomiast jest obserwatorem i obserwuje. Implementacja wzorca obserwator od podstaw w kodzie nieco wykracza poza zakres tego artykułu. Jeśli czujesz potrzebę dalszego rozwinięcia tematu, to daj znać w komentarzu. Przygotuję wtedy dedykowany artykuł.
Obserwatory w JavaScript
Przy tworzeniu aplikacji przeglądarkowych do dyspozycji dostajemy kilka Web API związanych z obserwacją. W tym wpisie skupię się na kilku z nich:
MutationObserver
,PerformanceObserver
,ResizeObserver
,IntersectionObserver
.
MutationObserver
Na pierwszy ogień weźmy MutationObservera
. Jak nazwa wskazuje, nasłuchuje on na mutacje, czyli zmiany w obserwowanym obiekcie. Aby nieco rozjaśnić temat, zapoznaj się z przykładem w kodzie.
const observer = new MutationObserver( data => {
console.log( 'mutation!', data )
} );
observer.observe( document.getElementById( 'foo' ), {
attributes: true,
characterData: true,
childList: true,
subtree: true,
} );
Na pierwszy rzut oka nie wydaje się to być zbyt skomplikowane. Na samym początku tworzymy obserwatora, któremu deklarujemy, co ma się wykonać, gdy zostanie zaobserwowane interesujące nas zdarzenie. Następnie za pomocą metody observe()
dodajemy do puli obserwowanych obiektów nowy obiekt wraz z parametrami, jakie nas interesują. Powyższy kod dodaje do puli obserwowanych obiektów element z id
równym foo zaś interesujące nas mutacje to: zmiany wartości atrybutów, zmiany wartości data, dodawanie oraz usuwanie elementów potomnych, oraz zmiany w elementach potomnych.
Mówiąc o atrybutach mamy na myśli zmiany takie jak, dodanie czy usunięcie klasy lub id w elemencie, zmiany atrybutu href
w linku czy wartości checked
w checkboxie. Oczywiście to tylko kilka przykładów — można ich podać znacznie więcej. Nie dotyczy to jednak atrybutów data-[wartość]
— za to odpowiedzialna jest opcja characterData
. Kolejna opcja to childList
, który obserwuje dodawanie i usuwanie elementów potomnych. Ostatnim parametrem jest subtree
, który propaguje obserwację na elementy potomne. W tym miejscu koniecznie należy zaznaczyć o używaniu tych parametrów z rozwagą. Obserwując na przykład element body
, z powyższą konfiguracją będziemy obserwowali każdą zmianę w każdym elemencie na stronie. To na pewno nie wpłynie pozytywnie na wydajność naszej aplikacji. Pamiętajmy zatem, aby korzystać z tego rozwiązania rozważnie. Oprócz powyższych opcji konfiguracji do dyspozycji mamy jeszcze:
attributeOldValue
– wartośćboolean
— pozwala na przechwycenie poprzedniej wartości atrybutu,characterDataOldValue
– wartośćboolean
— pozwala na przechwycenie poprzedniej wartości atrybutudata
,attributeFilter
–array
z interesującymi nas atrybutami — pozwala filtrować, na jakie atrybuty chcemy nasłuchiwać. Nie ma sensu nasłuchiwać na wszystkie atrybuty, w momencie, gdy interesuje nas na przykład tylko zmiana klasy elementu.
Nie zapomnijmy też o wyłączeniu obserwatora, gdy już go nie potrzebujemy. Posłuży nam do tego observer.disconnect()
.
PerformanceObserver
Wspomniałem już co nieco o wydajności przy MutationObsereverze
. Do mierzenia wydajności posłużyć nam może PerformanceObserver
. Tematowi wydajności przyjrzałem się bliżej w dedykowanym artykule. Jeśli jeszcze go nie przeczytałeś/aś, to zachęcam do zapoznania się. Myślę, że w tym przypadku również dobrze będzie zacząć od kawałka kodu.
function calc() {
let sum = 0;
performance.measure( 'sum' );
for( let i = 0; i <= 50000; i++ ) {
sum += i
}
performance.measure( 'sum' );
}
const observer = new PerformanceObserver( list => {
const entries = list.getEntries();
console.log( entries[ 1 ].duration - entries[ 0 ].duration )
} );
observer.observe( { entryTypes: [ 'measure' ] } );
document.getElementById( 'foo' ).addEventListener( 'click', function() {
calc();
} );
Tutaj również tworzymy nową instancję obserwatora i używamy metody observe()
, także za wiele się nie zmieniło. Jako argument instancji obserwatora podajemy, co ma się wykonać, natomiast w konfiguracji podajemy zdarzenia, jakie chcemy obserwować. W naszym wypadku nasłuchujemy na zdarzenie measure
. Powyższy kod powinien nam pokazać ile czasu (w milisekundach) zajmie dodanie do siebie liczb od 0 do 50000. Oczywiście do entryTypes
możemy przekazać inne wartości niż tylko measure
— ich pełną listę wraz z opisami znajdziesz na MDN.
ResizeObserver
Przy zapoznawaniu się z ResizeObserverem
miałem wątpliwości co do jego użyteczności. Przecież do dyspozycji mamy zdarzenie resize
, które nasłuchuje na zmianę wymiarów okna lub dokumentu. To w czym ResizeObserever
wygrywa z tradycyjnym podejściem, opartym na dodaniu listenera, jest możliwość nasłuchiwania na zmianę wymiarów elementów, nie tylko window
i document
.
Dotyczy to także zmian powodowanych przez na przykład dodanie lub usunięcie elementów z obserwowanego elementu. Co więcej, nie jesteśmy zmuszeni do szukania wymiarów elementu po jego właściwościach, gdyż mamy do nich dostęp bezpośrednio w callbacku.
const observer = new ResizeObserver( entries => {
for ( let entry of entries ) {
console.log( entry );
}
} );
observer.observe( document.querySelector( '.container' ) );
Użycie tego observera jest banalne, nie różni się niczym od poprzednich przypadków. Podczas tworzenia observera deklarujemy akcję do wykonania, a w metodzie observe()
podajemy interesujący nas element i to w zasadzie tyle.
IntersectionObserver
Ostatnim z obserwatorów, jaki weźmiemy na tapet, będzie IntersectionObserver
. Stosuje się go w momencie, gdy potrzebujemy obserwować, czy dany element jest obecny na ekranie, czy nie. Znajdzie to zastosowanie głównie w przypadku stron mobilnych oraz stron z dużą ilością treści. Genialnie to się sprawdzi przy implementacji np. lazy loadingu, czy infinite scrollingu. Kod w zasadzie nie będzie się znacząco różnił od poprzedniego przykładu.
const observer = new IntersectionObserver( entries => {
for ( let entry of entries ) {
console.log( entry );
}
} );
observer.observe( document.querySelector( '.gallery' ) );
Co ciekawe, w entry
mamy dostęp do właściwości intersectionRatio
, która mówi nam jaki procent elementu jest widoczny na ekranie.
Aby przestać obserwować dany element, należy użyć observer.unobserve( element )
. Natomiast, gdy całkowicie chcemy wyłączyć observera, to posłuży nam do tego observer.disconnect()
.
Podsumowanie
Standardowo zachęcam do pozostawienia komentarza i zajrzenia do źródeł — znajdziesz tam więcej szczegółów i przykładów. Jeśli dowiedziałeś/aś się czegoś nowego, to udostępnij ten wpis na swoich social mediach! 🙂
Źródła i materiały dodatkowe
- Wzorce projektowe – czym są i dlaczego warto je znać?
- Mierzenie wydajności aplikacji WWW
- Event reference
- Observer / Obserwator – część 1/2 – Wzorce Projektowe #04
- #2 Wzorce projektowe: Obserwator po raz kolejny
- Wzorzec projektowy obserwator
- MutationObserver API
- Podstawy działania UI — wzorzec obserwator
- Wzorzec obserwator w UI — podejścia scentralizowane
- MutationObserver
- PerformanceObserver
- Performance observer – Efficient access to performance data
- ResizeObserver: it’s like document.onresize for elements
- ResizeObserver
- IntersectionObserver
- Observer vs Pub-Sub pattern
Zapisz się na mailing i odbierz e-booka
Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.