chevron-left chevron-right

[JS] Jak obserwować zmiany obiektów w JavaScript? Natywny data-binding

Jak wszyscy wiemy, pisząc kod JavaScript dla naszej strony możemy wykorzystywać szereg eventów - zdarzeń DOM, które nas informują o tym co się dzieje z elementami na stronie. Mogą to być zdarzenia: load, submit, mousedown i wiele innych.

Lecz to nie wszystko co możemy obserwować za pomocą JavaScriptu. Jedną z rzeczy mniej znanych, ale bardzo użytecznych jest obserwacja zmian stanów wartości atrybutów elementów DOM lub obserwacja zmian wartości obiektów JavaScript. Po przeczytaniu tego artykułu będziesz mógł/mogła wykorzystać ten sposób w swoich projektach webowych.

Zobacz demo

Obserwacja zmian wartości atrybutów DOM za pomocą JS

Aby móc obserować zmiany wartości atrybutów (np. data atrybuty, value, src, itd., itp) elementów DOM to musimy skorzystać z funkcjonalności o nazwie Mutation Observer. Za jego pomocą będziemy w stanie obserwować i reagować na zmiany w drzewie DOM.

Do obiektu MutationObserver możemy przekazać odpowiedni zestaw parametrów, który pozwoli nam uzyskać informacje o zmianach które zachodzą w danym elemencie na stronie. Ich wartości zawsze ustawiamy albo na true albo na false. Domyślnie, zawsze będzie to false.

Własność Opis
childList Czy chcesz obserwować zmiany elementów DOM wewnątrz wybranego elementu?
attributes Czy chcesz obserwować zmiany wartości atrybutów elementu DOM?
characterData Czy chcesz obserwować zmiany tekstu wewnątrz elementu DOM?
subtree Czy chcesz obserwować zmiany elementów DOM wewnątrz elementu oraz zmiany w DOM w elementach znajdujących się w wybranym elemencie.
attributeOldValue Czy chcesz zapamiętać poprzednią wartość zmienionego atrybutu elementu?
characterDataOldValue Czy chcesz zapamiętach poprzedni zmieniony tekst elementu?
attributeFilter Jedyny przypadek, gdy zamiast wartości true lub false ustawiamy tablicę zawierającą listę atrybutów, które chcemy rzeczywiście obserwować.

Jego zastosowanie wygląda następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    var MutationObserver = window.MutationObserver || window.WebKitMutationObserver,
        attributeChangeCallback = function (mutations) {
            mutations.forEach(function (mutation) {
                console.log('mutation', mutation);
                [ ... jakikolwiek kod który ma być uruchomiony gdy zajdą zmiany wartości w atrybutach obiektów DOM ... ]
            });
        },
        observer = new MutationObserver(attributeChangeCallback), // nowa instancja MutationObserver
        domElement = document.querySelector('.element-to-observe'); // jakiś element DOM którego zmiany wartości atrybutów będziemy obserwować, np. '<div class=".element-to-observe" data-type="default"></div>'
 
    observer.observe(domElement, {
        attributes: true,
        attributeOldValue: true
    });

Mając powyższy kod, za każdym razem gdy zmieni się wartość atrybutu class bądź data-type, zostanie uruchomiona funkcja attributeChangeCallback. Ta funkcja (callback) będzie mogła reagować na zmiany. W przypadku powyżej, w konsoli w przeglądarce pojawi log dotyczący pojedynczej mutacji, który będzie zawierał następujące informacje:

Własność Typ własności Opis
type String Zwraca informację odnośnie typu mutacji. Jeśli zajdzie zmiana w atrybutach, to zwraca attributes. Jeśli zajdzie zmiana w tekście wewnątrz elementu, to zwróci informację characterData. Natomiast jeśli będzie to zmiana elementów DOM wewnątrz wybranego elementu, to własność będzie zawierała informację childList.
target Node Informacje o elemencie w którym zaszły zmiany. Jest to obiekt typu Node.
addedNodes NodeList Zwraca listę elementów DOM, które zostały dodane do wybranego elementu. Jeśli taka zmiana jest obserwowana.
removedNodes NodeList Zwraca listę elementów DOM, które zostały usunięte z wybranego elementu. Jeśli taka zmiana jest obserwowana.
previousSibling Node Zwraca obiekt dotyczący elementu poprzedzającego element dodany/usunięty lub null
nextSibling Node Zwraca obiekt dotyczący elementu następnego po elemencie dodanym/usuniętym lub null
attributeName String Zwraca nazwę zmienionego atrybutu lub null
attributeNamespace String Zwraca przestrzeń nazw w której znajduje się zmieniony atrybut lub null
oldValue String Zwraca poprzedni tekst, który został zastąpiony nowym wewnątrz elementu lub wartość atrybutu elementu. Nie zwraca on elementów DOM, które zostały zmienione. Aby uzyskać listę zmian w DOM należy wykorzystać removedNodes.

Obserwacja zmian wartości elementów DOM za pomocą JS

Kod, który będzie obserwował zmiany w drzewie DOM wewnątrz elementu będzie wyglądał następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    var MutationObserver = window.MutationObserver || window.WebKitMutationObserver,
        domChangeCallback = function (mutations) {
            mutations.forEach(function (mutation) {
                console.log('mutation', mutation);
                [ ... jakikolwiek kod który ma być uruchomiony gdy zajdą zmiany w drzewie DOM elementu ... ]
            });
        },
        observer = new MutationObserver(domChangeCallback), // nowa instancja MutationObserver
        domElement = document.querySelector('.element-to-observe'); // jakiś element DOM którego zmiany wartości atrybutów będziemy obserwować, np. '<div class=".element-to-observe" data-type="default"></div>'
 
    observer.observe(domElement, {
        childList: true,
        subtree: true
    });

Jak widać, kod nieznacznie się różni od poprzedniego kodu. Tak naprawdę główną różnicą będą parametry przekazane do obiektu MutationObserver oraz callback, który będzie obsługiwał wybrane zdarzenie.

W przypadku, gdybyśmy chcieli obserwować zmiany tekstu, to jako parametry obiektu możemy ustawić:

1
2
3
4
    textChangeParams = {
        characterData: true,
        characterDataOldValue: true
    };

I wstawiamy go jako drugi parametr funkcji observer.observe(). Co ciekawe, w przeglądarce Firefox zmiany tekstu będą obserwowane nawet wtedy, gdy do observer.observe() przekażemy parametry dotyczące childList oraz subtree.

Obserwacja zmian wartości właśności obiektów za pomocą JS

Jeśli zdarzyło Ci się korzystać z Angular.js lub jakiegokolwiek innego frameworka JavaScript gdzie jest zaimplementowany data-binding, to z pomocą funkcjonalności obserwowania zmian w obiektach - Object.observe() będziesz w stanie osiągnąć niemal to samo zachowanie. Twoja aplikacja będzie w stanie reagować na zmiany wartości w obiektach.

Dla przykładu, wyibraźmy sobie że mamy obiekt z danymi użytkownika. Po każdej zmianie nazwy użytkownika będziemy chcieli zaktualizować napis wyświetlany na stronie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    var user = {
        name: 'Piotr Nalepa',
        info: {}
    },
    userNameContainer = document.querySelector('#username'),
    classNameChanged = '.name-changed',
    userNameChangedTimeout,
    updateUserName = function () {
        userNameContainer.innerHTML = user.name;
        userNameContainer.classList.add(classNameChanged);
 
        window.clearTimeout(userNameChangedTimeout);
 
        userNameChangedTimeout = window.setTimeout(function () {
            userNameContainer.classList.remove(classNameChanged);
        }, 500);
    },
    objectChangedCallback = function (changes) {
        changes.forEach(function(change) {
            if (change.name === 'name') {
                updateUserName();
            }
        });
    };
 
    updateUserName();
    Object.observe(user, objectChangedCallback);

Nic nie stoi na przeszkodzie, aby obserwowanie zmian obiektów sprzężyć z obserwowaniem zmian w DOM. W ten sposób możemy osiągnąć tzw. 2-way data-binding.

Funkcja Object.observe() przyjmuje 3 parametry:

Nazwa Opis
obj Obiekt który chcemy obserwować
callback Funkcja, która będzie uruchamiana przy każdej zmianie wartości obiektów. Funkcja jako parametr przyjmuje tablicę obiektów z informacjami o zmianach w wybranym obiekcie. Obiekt z informacjami o zmianach posiada następujące informacje:
  • name - nazwa własności, której wartość została zmieniona,
  • object - nowy stan obiektu, po zmianie;
  • type - rodzaj zmiany: add, update, delete;
  • oldValue - wartość przed zmianą.
acceptList Lista typów zmian, które mają być obserwowane w danym obiekcie. Domyślnie, wartość tego parametru to: ['add', 'update', 'delete', 'reconfigure', 'setPrototype', 'preventExtensions'].

Polyfille

Aby móc korzystać z pełni możliwości w większości przeglądarek, niezbędne będą dodatkowe bilioteki, tzw. polyfille, które uzupełnią brakujące funkcjonalności w przeglądarkach.

Mutation Observer polyfill dla DOM

Jeśli chodzi obserwowanie zmiany w drzewie DOM elementów, to polecanym polyfillem będzie biblioteka od ludzi zajmujących się Web Components.

Object.observe polyfill

Natomiast, dla Object.observe() polecanym polyfillem będzie biblioteka stworzona przez twórców Polymera.

Podsumowanie

Mam nadzieję, że funkcjonalności, które omówiłem w tym wpisie pozwolą Ci na pisanie jeszcze ciekawszych skryptów JavaScript niż do tej pory. Za ich pomocą, w wielu przypadkach, nie trzeba korzystać żadnych zewnętrznych narzędzi aby mieć możliwość korzystania z tych ficzerów.

Co do wsparcia przeglądarek to dla Mutation Observer wsparcie wygląda następująco:

mutation-observer-browser-support

Natomiast, jeśli chodzi o Object.observe() to z tym jest gorzej. Ze względu na to, że jest to część składowa standardu ECMAScript 7 to jego wsparcie w przeglądarkach wygląda dość mizernie. Generalnie, przeglądarki z silnikiem Webkit wspierają ten standard, a wszystkie inne wymagają dodatkowych bibliotek (polyfilli):

object-observe-browser-support

  • Powiem wprost: podpieprzyłeś mi temat. Miałem za niedługo napisać podobny artek… Eh – i tak napiszę 😉

    Najbardziej mnie zastanawia fakt, że de facto obydwa mechanizmy są rozwijane głównie na potrzeby Web Components obecnie – zresztą to samo dzieje się z custom eventami. Zaczęło się od niewinnych custom tagów, a teraz połowa DOM + ficzery w ES7 są pisane dla potrzeb tej technologii.

  • 1). Ten temat mi chodził po głowie już od miesiąca.
    2). Skoro to jednak implementują to dobrze. Jest to potrzebne. Z tego powodu, że jest to tworzone w wyniku potrzeb Web Components, to nie jest żaden problem dla mnie. Dobrze, że starają się rozwijać technologię.

  • Jasne, dobrze że rozwijają. Problem polega na tym, że takimi rzeczami zwykli devowie są średnio zainteresowani, bo takie rzeczy spoczywają w bebechach frameworków takich jak Angular. Zaryzykowałbym stwierdzenie, że obserwatorów głównie będą używać libmakerzy, nie zaś „normalni” devowie. Tak to widzę.

  • Być może masz rację, ale to raczej jest kwestia uświadomienia web developerów, że coś takiego mogą zrobić. Jeśli ktoś im o tym nie powie, to nie będą wiedzieć.

  • Szkoda, że Object.observe usunięto 🙁