Wiązanie danych obrotów za pomocą funkcji Object.observe()

Addy Osmanii
Addy Osmani

Wstęp

Nadchodzi rewolucja. Pojawił się nowy dodatek do JavaScriptu, który zmieni wszystko, co Twoim zdaniem wiesz o wiązaniach danych. Zmieni się również liczba bibliotek MVC, które podchodzą do obserwacji modeli na potrzeby edycji i aktualizacji. Co powiesz na poprawę wydajności aplikacji, których celem jest obserwacja nieruchomości?

Miło mi poinformować, że aplikacja Object.observe() jest już dostępna w wersji stabilnej Chrome 36. THE CROWD GOES WILD].

Object.observe(), część przyszłego standardu ECMAScript, to metoda asynchronicznego obserwowania zmian w obiektach JavaScript bez konieczności posiadania osobnej biblioteki. Umożliwia obserwatorowi otrzymywanie uporządkowanych w czasie sekwencji zmian opisujących zbiór zmian, które zaszły w zbiorze obserwowanych obiektów.

// Let's say we have a model with data
var model = {};

// Which we then observe
Object.observe(model, function(changes){

    // This asynchronous callback runs
    changes.forEach(function(change) {

        // Letting us know what changed
        console.log(change.type, change.name, change.oldValue);
    });

});

Za każdym razem, gdy wprowadzasz zmianę, są raportowane te dane:

Zmiana została zgłoszona.

Za pomocą usługi Object.observe() (lubię ją nazywać O.o() lub Oooooooo) możesz zaimplementować dwukierunkowe wiązanie danych bez konieczności używania platformy.

Nie oznacza to jednak, że nie należy ich używać. W przypadku dużych projektów ze skomplikowaną logiką biznesową schematy oparte na opiniach są nieocenione i warto ich używać. Upraszczają one orientację nowym programistom, wymagają mniej konserwacji kodu i narzucają wzorce wykonywania typowych zadań. Jeśli nie potrzebujesz biblioteki, możesz skorzystać z mniejszych i bardziej ukierunkowanych bibliotek, np. Polymer (która już korzysta z funkcji O.o()).

Nawet jeśli często używasz platformy lub biblioteki MV*, funkcja O.o() może zapewnić im lepsze wyniki w postaci szybszej i prostszej implementacji przy zachowaniu tego samego interfejsu API. Na przykład w zeszłym roku firma Angular odkryła, że test porównawczy, w ramach którego wprowadzano zmiany w modelu, trwało 40 ms na aktualizację, a funkcja O.o() – 1–2 ms na aktualizację (poprawa 20–40 razy szybciej).

Powiązanie danych bez konieczności pisania skomplikowanego kodu oznacza również, że nie trzeba już wyszukiwać zmian, co wydłuży czas pracy na baterii.

Jeśli korzystasz już z O.o(), przejdź od razu do wprowadzenia do funkcji lub przeczytaj dalej, aby dowiedzieć się więcej o problemach, jakie ona rozwiązuje.

Co chcemy obserwować?

Gdy mówimy o obserwacji danych, zazwyczaj mamy na myśli obserwowanie określonych typów zmian:

  • Zmiany w nieprzetworzonych obiektach JavaScript
  • Po dodaniu, zmianie lub usunięciu usług
  • Gdy tablice mają elementy połączone na zewnątrz i na zewnątrz
  • Zmiany w prototypie obiektu

Znaczenie wiązania danych

Powiązanie danych staje się ważne, gdy zależy Ci na rozdzieleniu elementów sterujących i widoku modelu. HTML to świetny mechanizm deklaracyjny, ale jest całkowicie statyczny. Najlepiej jest zadeklarować relację między danymi a DOM, aby ją aktualizować. W ten sposób wykorzystujesz dane i oszczędzasz czas na pisanie naprawdę powtarzającego się kodu, który po prostu wysyła dane do i z DOM między wewnętrznym stanem aplikacji a serwerem.

Powiązanie danych jest szczególnie przydatne, gdy masz złożony interfejs użytkownika, w którym musisz powiązać w modelach danych relacje między wieloma usługami w modelach danych z wieloma elementami w widokach. Jest to dość powszechne w tworzonych obecnie aplikacjach jednostronicowych.

Opracowaliśmy metodę natywnej obserwacji danych w przeglądarce, aby dać platformom JavaScript (i napisanym przez Ciebie małym biblioteczkom narzędziowym) sposób obserwowania zmian w danych w modelach bez polegania na niektórych powolnych atakach stosowanych obecnie na całym świecie.

Jak obecnie wygląda świat

brudne sprawdzanie

Gdzie widziałeś(-aś) już powiązywanie danych? Jeśli do tworzenia aplikacji internetowych używasz nowoczesnej biblioteki MV* (np.Angular, Knockout), prawdopodobnie jesteś przyzwyczajony do wiązania danych modelu z DOM. Dla przypomnienia podajemy przykład aplikacji do obsługi list telefonów, w której wiążąmy wartość każdego telefonu w tablicy phones (zdefiniowanej w języku JavaScript) z elementem listy, tak aby nasze dane i interfejs były zawsze zsynchronizowane:

<html ng-app>
  <head>
    ...
    <script src='angular.js'></script>
    <script src='controller.js'></script>
  </head>
  <body ng-controller='PhoneListCtrl'>
    <ul>
      <li ng-repeat='phone in phones'>
        
        <p></p>
      </li>
    </ul>
  </body>
</html>

oraz kod JavaScript kontrolera:

var phonecatApp = angular.module('phonecatApp', []);

phonecatApp.controller('PhoneListCtrl', function($scope) {
  $scope.phones = [
    {'name': 'Nexus S',
     'snippet': 'Fast just got faster with Nexus S.'},
    {'name': 'Motorola XOOM with Wi-Fi',
     'snippet': 'The Next, Next Generation tablet.'},
    {'name': 'MOTOROLA XOOM',
     'snippet': 'The Next, Next Generation tablet.'}
  ];
});

Za każdym razem, gdy zmienią się dane modelu bazowego, lista w DOM jest aktualizowana. Jak Angular to osiąga? Za kulisami sprawdza się coś takiego.

Brudne sprawdzanie

Podstawową koncepcją tego narzędzia jest to, że za każdym razem, gdy dane mogą się zmienić, biblioteka musi sprawdzać, czy dane się zmieniły, analizując podsumowanie lub cykl zmian. W przypadku Angular cykl podsumowania identyfikuje wszystkie wyrażenia, które mają być monitorowane, aby sprawdzić, czy nie zaszły jakieś zmiany. Wie o poprzednich wartościach modelu i jeśli ulegną one zmianie, wywoływane jest zdarzenie zmiany. Główną korzyścią dla programistów jest możliwość wykorzystania nieprzetworzonych danych obiektów JavaScript, które są przyjemne w użyciu i dość dobrze się komponują. Wadą jest to, że działa niezgodnie z algorytmem i może być bardzo kosztowna.

Brudne sprawdzanie.

Koszt tej operacji jest proporcjonalny do łącznej liczby obserwowanych obiektów. Być może będę musiała dużo sprawdzić. Może też wymagać wywołania błędnego sprawdzenia, gdy dane mogą się zmienić. Istnieje w tym celu wiele sprytnych sztuczek. Nie wiem, czy to kiedykolwiek będzie idealne.

Ekosystem internetowy powinien mieć większą zdolność do wprowadzania innowacji i rozwijania własnych mechanizmów deklaracyjnych, np.

  • Systemy modeli oparte na ograniczeniach
  • Systemy automatycznej trwałości (np.trwałe zmiany w IndexedDB lub localStorage)
  • Obiekty kontenera (Ember, szkielet)

Obiekty Container to obiekty, w których platforma tworzy obiekty, które zawierają dane. Takie osoby mają dostęp do danych i mogą rejestrować to, co ustawisz lub otrzymujesz, oraz będą mogli rozpowszechniać je wewnętrznie. To działa dobrze. Jest stosunkowo wydajny i ma dobry algorytm. Przykładowe obiekty kontenera korzystające z kodu Ember znajdziesz poniżej:

// Container objects
MyApp.president = Ember.Object.create({
  name: "Barack Obama"
});
 
MyApp.country = Ember.Object.create({
  // ending a property with "Binding" tells Ember to
  // create a binding to the presidentName property
  presidentNameBinding: "MyApp.president.name"
});
 
// Later, after Ember has resolved bindings
MyApp.country.get("presidentName");
// "Barack Obama"
 
// Data from the server needs to be converted
// Composes poorly with existing code

Wydatki na sprawdzenie, co się zmieniło, są proporcjonalne do liczby zmian. Kolejnym problemem jest używanie obiektu innego rodzaju. Aby obiekty te były obserwowalne, należy dokonać konwersji z danych otrzymywanych z serwera na te obiekty.

Nie działa to szczególnie dobrze w przypadku dotychczasowego kodu JavaScript, ponieważ większość kodu zakłada, że może działać na nieprzetworzonych danych. Nie dotyczy tych wyspecjalizowanych obiektów.

Introducing Object.observe()

Najlepiej byłoby, gdyby łączyło to, co najlepsze w obu rozwiązaniach – czyli możliwość obserwowania danych przy obsłudze obiektów nieprzetworzonych danych (zwykłe obiekty JavaScript) przy zastosowaniu operatora ORAZ bez konieczności ciągłego sprawdzania wszystkich elementów. Działa poprawnie algorytmicznie. Coś, co dobrze się komponuje i zagra na platformie. To piękno, które prezentuje Object.observe().

Dzięki temu możemy obserwować obiekt, wprowadzać mutacje właściwości i wyświetlać raporty o zmianach dotyczących tego, co się zmieniło. Ale dosyć teorii. Przejdźmy teraz do kodu.

Object.observe()

Object.observe() i Object.unobserve()

Wyobraźmy sobie prosty obiekt w języku JavaScript, który reprezentuje model:

// A model can be a simple vanilla object
var todoModel = {
  label: 'Default',
  completed: false
};

Możemy wtedy określić wywołanie zwrotne w przypadku każdego wprowadzenia mutacji (zmian) w obiekcie:

function observer(changes){
  changes.forEach(function(change, i){
      console.log('what property changed? ' + change.name);
      console.log('how did it change? ' + change.type);
      console.log('whats the current value? ' + change.object[change.name]);
      console.log(change); // all changes
  });
}

Zmiany te można obserwować za pomocą funkcji O.o(), która przekazuje obiekt jako pierwszy argument, a wywołanie zwrotne jako drugi:

Object.observe(todoModel, observer);

Zacznijmy wprowadzać zmiany w obiekcie modelu Todos:

todoModel.label = 'Buy some more milk';

W konsoli otrzymujesz przydatne informacje. Wiemy, jaka usługa się zmieniła, jak została zmieniona i jaka jest nowa wartość.

Raport konsoli

Super! Nie ma sprawy! Nagrobek powinien być wyrzeźbiony w Comic Sans. Zmieńmy inną właściwość. Tym razem completeBy:

todoModel.completeBy = '01/01/2014';

Jak widać, po raz kolejny otrzymaliśmy raport o zmianach:

Zmień raport.

Doskonale. A co, jeśli zdecydujemy się usunąć właściwość „completed” z naszego obiektu:

delete todoModel.completed;
Ukończono

Jak widzimy, raport ze zwróconych zmian zawiera informacje o usunięciu treści. Zgodnie z oczekiwaniami nowa wartość właściwości jest teraz niezdefiniowana. Teraz wiemy, że możesz sprawdzić, kiedy usługi zostaną dodane. Kiedy zostały usunięte. Zasadniczo zbiór właściwości obiektu („nowy”, „usunięty”, „zmieniony”) oraz zmienia się jego prototyp (proto).

Tak jak w każdym systemie obserwacji, istnieje także metoda pozwalająca przestać wychwytywać zmiany. W tym przypadku jest to Object.unobserve(), który ma taki sam podpis jak O.o(), ale może być wywołany w ten sposób:

Object.unobserve(todoModel, observer);

Jak widać poniżej, wszelkie mutacje wprowadzone w obiekcie po jego uruchomieniu nie powodują już zwrócenia listy rekordów zmian.

Mutacje

Określanie zmian zainteresowań

Omówiliśmy już podstawy pobierania listy zmian zaobserwowanego obiektu. Co zrobić, jeśli interesuje Cię tylko część zmian wprowadzonych w obiekcie, a nie wszystkie? Każdy z nas potrzebuje filtra spamu. No cóż, obserwatorzy mogą za pomocą listy akceptacji wskazać tylko te typy zmian, o których chcą wiedzieć. Można ją określić za pomocą trzeciego argumentu funkcji O.o() w następujący sposób:

Object.observe(obj, callback, optAcceptList)

Przyjrzyjmy się przykładowi, jak można to wykorzystać:

// Like earlier, a model can be a simple vanilla object

var todoModel = {
  label: 'Default',
  completed: false

};


// We then specify a callback for whenever mutations 
// are made to the object
function observer(changes){
  changes.forEach(function(change, i){
    console.log(change);
  })

};

// Which we then observe, specifying an array of change 
// types we're interested in

Object.observe(todoModel, observer, ['delete']);

// without this third option, the change types provided 
// default to intrinsic types

todoModel.label = 'Buy some milk'; 

// note that no changes were reported

Jeśli jednak teraz usuniemy etykietę, zmiany tego typu będą rejestrowane:

delete todoModel.label;

Jeśli nie określisz listy typów akceptowania dla O.o(), domyślnie będą to „wewnętrzne” typy zmian obiektów (add, update, delete, reconfigure, preventExtensions (gdy obiekt staje się nierozbudowy, którego nie można obserwować)).

Powiadomienia

Funkcja O.o() ma też funkcję powiadomień. Nie są one irytujące, ale raczej irytujące. Powiadomienia są podobne do obserwatorów zmian. Odbywa się to na końcu mikrozadania. W kontekście przeglądarki prawie zawsze będzie to znajdować się na końcu bieżącego modułu obsługi zdarzeń.

Termin jest dobry, bo zazwyczaj jedna jednostka pracy została ukończona, a obserwatorzy mogą zająć się swoimi zadaniami. To przyjemny model przetwarzania turowego.

Proces korzystania z powiadomienia wygląda mniej więcej tak:

Powiadomienia

Przyjrzyjmy się przykładowi praktycznego wykorzystania powiadomień do definiowania niestandardowych powiadomień dotyczących pobierania lub ustawiania właściwości obiektu. Obserwuj komentarze, które pojawiają się tutaj:

// Define a simple model
var model = {
    a: {}
};

// And a separate variable we'll be using for our model's 
// getter in just a moment
var _b = 2;

// Define a new property 'b' under 'a' with a custom
// getter and setter

Object.defineProperty(model.a, 'b', {
    get: function () {
        return _b;
    },
    set: function (b) {

        // Whenever 'b' is set on the model
        // notify the world about a specific type
        // of change being made. This gives you a huge
        // amount of control over notifications
        Object.getNotifier(this).notify({
            type: 'update',
            name: 'b',
            oldValue: _b
        });

        // Let's also log out the value anytime it gets
        // set for kicks
        console.log('set', b);

        _b = b;
    }
});

// Set up our observer
function observer(changes) {
    changes.forEach(function (change, i) {
        console.log(change);
    })
}

// Begin observing model.a for changes
Object.observe(model.a, observer);
Konsola powiadomień

Tutaj znajdziesz informacje o zmianach wartości właściwości danych („aktualizacja”). Dowolna inna wartość raportowana przez implementację obiektu (notifier.notifyChange()).

Wieloletnie doświadczenie w korzystaniu z platformy internetowej nauczyło nas, że podejście synchroniczne to najłatwiejszy sposób do pracy nad koncentracją. Problem polega na tym, że powstaje zasadniczo niebezpieczny model przetwarzania. Gdy piszesz kod i piszesz, że chcesz zaktualizować właściwość obiektu, to raczej nie powinno tak się stać, gdy aktualizacja właściwości tego obiektu mogłaby spowodować zaproszenie do wykorzystania określonego kodu. Nie zalecamy unieważniania założeń w trakcie wykonywania funkcji.

Obserwatorzy nie powinni chcieć, żeby ktoś zwracał się do Ciebie, gdy ktoś jest na samym środku. W ten sposób nie chcemy Cię prosić o pracę w niekonsekwencjach na świecie. Trzeba sprawdzić znacznie więcej błędów. Próbuję tolerować znacznie więcej złych sytuacji i ogólnie jest to trudny model. Obsługa asynchronicznej jest trudniejsza w obsłudze, ale pod koniec dnia jest lepszym modelem.

Rozwiązaniem tego problemu są syntetyczne rekordy zmian.

Syntetyczne rekordy zmian

Jeśli chcesz mieć akcesory lub obliczone właściwości, Twoim obowiązkiem jest powiadomienie o zmianie tych wartości. Wymaga to nieco więcej pracy, ale zostało zaprojektowane jako pierwszorzędna funkcja tego mechanizmu, dzięki czemu powiadomienia będą dostarczane wraz z pozostałymi powiadomieniami pochodzącymi z obiektów danych. Z usług danych.

Syntetyczne rekordy zmian

Obserwowanie akcesorów i obliczonych właściwości można rozwiązać za pomocą funkcji notifier.notify – innej części O.o(). Większość systemów obserwacji wymaga pewnej formy obserwacji wartości pochodnych. Możesz to zrobić na wiele sposobów. O.o nie ocenia, czy dany sposób jest „właściwy”. Obliczone właściwości powinny być akcesorami, które notify o zmianie stanu wewnętrznego (prywatnego).

Twórcy stron internetowych również powinni oczekiwać, że biblioteki ułatwią powiadamianie i różne sposoby korzystania z obliczonych właściwości (i ułatwią stosowanie powtarzalnych schematów).

Skonfigurujmy kolejny przykład, który jest klasą kręgu. Chodzi o to, że mamy to koło i właściwość promienia. W tym przypadku promień jest akcesorem, a gdy jego wartość ulegnie zmianie, będzie sam siebie powiadamiać o zmianie wartości. Wprowadzone zmiany zostaną zastosowane wraz ze wszystkimi innymi zmianami wprowadzonymi w tym lub innym obiekcie. Zasadniczo, jeśli wdrażasz obiekt, potrzebujesz właściwości syntetycznych lub obliczeniowych albo musisz wybrać strategię ich działania. Gdy to zrobisz, zostanie on dopasowany do całego Twojego systemu.

Aby zobaczyć, jak to działa w Narzędziach deweloperskich, pomiń kod.

function Circle(r) {
  var radius = r;
 
  var notifier = Object.getNotifier(this);
  function notifyAreaAndRadius(radius) {
    notifier.notify({
      type: 'update',
      name: 'radius',
      oldValue: radius
    })
    notifier.notify({
      type: 'update',
      name: 'area',
      oldValue: Math.pow(radius * Math.PI, 2)
    });
  }
 
  Object.defineProperty(this, 'radius', {
    get: function() {
      return radius;
    },
    set: function(r) {
      if (radius === r)
        return;
      notifyAreaAndRadius(radius);
      radius = r;
    }
  });
 
  Object.defineProperty(this, 'area', {
    get: function() {
      return Math.pow(radius, 2) * Math.PI;
    },
    set: function(a) {
      r = Math.sqrt(a/Math.PI);
      notifyAreaAndRadius(radius);
      radius = r;
    }
  });
}
 
function observer(changes){
  changes.forEach(function(change, i){
    console.log(change);
  })
}
Konsola syntetycznych rekordów zmian

Właściwości metody dostępu

Krótka uwaga na temat właściwości akcesora. Wspomnialiśmy wcześniej, że w przypadku właściwości danych można obserwować tylko zmiany wartości. Nie dotyczy usług obliczonych ani akcesorów. Wynika to z tego, że w JavaScript nie jest zaobserwowane zmiany wartości akcesorów. Akcesorium to po prostu zbiór funkcji.

Jeśli przypiszesz uprawnienia do akcesora, JavaScript po prostu wywoła tę funkcję i z punktu widzenia nic się nie zmieni. Pozwoliło to tylko na uruchomienie kodu.

Problem polega na semantycznym przypisaniu do wartości 5. Powinniśmy wiedzieć, co się tu wydarzyło. Tego problemu nie da się rozwiązać. Ten przykład pokazuje, dlaczego tak jest. System nie jest w stanie stwierdzić, co to znaczy, bo może to być dowolny kod. W tym przypadku może zrobić, co tylko zechce. Aktualizuje wartość przy każdym dostępie, więc pytanie, czy nastąpiła zmiana, nie ma sensu.

Obserwowanie wielu obiektów przy użyciu jednego wywołania zwrotnego

Kolejnym wzorcem możliwym przez funkcję O.o() jest koncepcja pojedynczego obserwatora wywołania zwrotnego. Dzięki temu jedno wywołanie zwrotne może być używane jako „obserwator” dla wielu różnych obiektów. Wywołanie zwrotne zwróci pełny zestaw zmian do wszystkich obserwowanych obiektów na „końcu mikrozadania” (zwróć uwagę na podobieństwo do funkcji Obserwatorzy mutacji).

Obserwowanie wielu obiektów przy użyciu jednego wywołania zwrotnego

Duże zmiany

Być może pracujesz nad naprawdę dużą aplikacją i regularnie musisz wprowadzać w niej zmiany na dużą skalę. W przypadku obiektów może być konieczne opisywanie większych zmian semantycznych, które będą miały wpływ na wiele właściwości, w bardziej kompaktowy sposób (zamiast przekazywania mnóstwa zmian właściwości).

Zastosowanie O.o() pomaga w tym dwóm konkretnym funkcjom: notifier.performChange() i notifier.notify(), które już wprowadziliśmy.

Duże zmiany

Na przykładzie tego, jak można opisać zmiany na dużą skalę, definiujemy obiekt Thingy za pomocą narzędzi matematycznych (mnożenie, zwiększanie, przyrostA). Za każdym razem, gdy narzędzie jest używane, informuje system, że zbiór utworów stanowi określony typ zmiany.

Na przykład: notifier.performChange('foo', performFooChangeFn);

function Thingy(a, b, c) {
  this.a = a;
  this.b = b;
}

Thingy.MULTIPLY = 'multiply';
Thingy.INCREMENT = 'increment';
Thingy.INCREMENT_AND_MULTIPLY = 'incrementAndMultiply';


Thingy.prototype = {
  increment: function(amount) {
    var notifier = Object.getNotifier(this);

    // Tell the system that a collection of work comprises 
    // a given changeType. e.g
    // notifier.performChange('foo', performFooChangeFn);
    // notifier.notify('foo', 'fooChangeRecord');
    notifier.performChange(Thingy.INCREMENT, function() {
      this.a += amount;
      this.b += amount;
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.INCREMENT,
      incremented: amount
    });
  },

  multiply: function(amount) {
    var notifier = Object.getNotifier(this);

    notifier.performChange(Thingy.MULTIPLY, function() {
      this.a *= amount;
      this.b *= amount;
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.MULTIPLY,
      multiplied: amount
    });
  },

  incrementAndMultiply: function(incAmount, multAmount) {
    var notifier = Object.getNotifier(this);

    notifier.performChange(Thingy.INCREMENT_AND_MULTIPLY, function() {
      this.increment(incAmount);
      this.multiply(multAmount);
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.INCREMENT_AND_MULTIPLY,
      incremented: incAmount,
      multiplied: multAmount
    });
  }
}

Następnie definiujemy 2 obserwatorów naszego obiektu. Jeden z nich dostarcza informacji o zmianach, a drugi dostarcza informacji o konkretnych zdefiniowanych przez nas typach akceptacji (Thingy.INCREMENT, Thingy.MULTIPLY, Thingy.INCREMENT_AND_MULTIPLY).

var observer, observer2 = {
    records: undefined,
    callbackCount: 0,
    reset: function() {
      this.records = undefined;
      this.callbackCount = 0;
    },
};

observer.callback = function(r) {
    console.log(r);
    observer.records = r;
    observer.callbackCount++;
};

observer2.callback = function(r){
    console.log('Observer 2', r);
}


Thingy.observe = function(thingy, callback) {
  // Object.observe(obj, callback, optAcceptList)
  Object.observe(thingy, callback, [Thingy.INCREMENT,
                                    Thingy.MULTIPLY,
                                    Thingy.INCREMENT_AND_MULTIPLY,
                                    'update']);
}

Thingy.unobserve = function(thingy, callback) {
  Object.unobserve(thingy);
}

Możemy teraz zacząć grę z tym kodem. Zdefiniujmy nowy obiekt:

var thingy = new Thingy(2, 4);

Obserwuj go, a potem wprowadź zmiany. Ale super. TAK BARDZO!

// Observe thingy
Object.observe(thingy, observer.callback);
Thingy.observe(thingy, observer2.callback);

// Play with the methods thingy exposes
thingy.increment(3);               // { a: 5, b: 7 }
thingy.b++;                        // { a: 5, b: 8 }
thingy.multiply(2);                // { a: 10, b: 16 }
thingy.a++;                        // { a: 11, b: 16 }
thingy.incrementAndMultiply(2, 2); // { a: 26, b: 36 }
Duże zmiany

Cała zawartość „funkcji wykonywania” jest uważana za dzieło „dużej zmiany”. Obserwatorzy, którzy zaakceptowali „dużą zmianę”, otrzymają tylko rekord o „dużej zmianie”. Obserwatorzy, którzy nie otrzymają podstawowych zmian wynikających z pracy wykonanej przez „wykonanie funkcji”.

Obserwowanie tablic

Od jakiegoś czasu mówimy o obserwowaniu zmian obiektów, ale co z tablicami? Świetne pytanie. Gdy ktoś mi powie: „Świetne pytanie”. Nigdy nie słyszę ich odpowiedzi, bo sam lubię gratulować sobie tak świetnych pytań, ale jestem znudzona. Mamy też nowe metody pracy z tablicami.

Array.observe() to metoda, która traktuje duże zmiany na dużą skalę, np. splice, unshift lub inne elementy, które pośrednio zmieniają długość, jako rekord zmiany „sprzętu”. Wewnętrznie korzysta z notifier.performChange("splice",...).

Oto przykład, w którym obserwujemy „tablicę” modelu i w podobny sposób uzyskujemy listę zmian, gdy w danych bazowych pojawią się jakiekolwiek zmiany:

var model = ['Buy some milk', 'Learn to code', 'Wear some plaid'];
var count = 0;

Array.observe(model, function(changeRecords) {
  count++;
  console.log('Array observe', changeRecords, count);
});

model[0] = 'Teach Paul Lewis to code';
model[1] = 'Channel your inner Paul Irish';
Obserwowanie tablic

Występy

Wpływ funkcji O.o() na wydajność obliczeniową można traktować jak pamięć podręczną odczytu. Ogólnie pamięć podręczna jest doskonałym rozwiązaniem, gdy (w kolejności według ważności):

  1. Częstotliwość odczytów dominuje nad częstotliwością zapisu.
  2. Możesz utworzyć pamięć podręczną o stałej ilości pracy wymaganej podczas zapisu, aby zwiększyć wydajność algorytmów podczas odczytu.
  3. Stałe spowolnienie zapisu jest dopuszczalne.

Funkcja O.o() jest przeznaczona dla przypadków użycia takich jak 1).

Aby przeprowadzić brudną kontrolę, należy przechowywać kopię wszystkich obserwowanych danych. Oznacza to, że w przypadku błędu O.o() koszty pamięci strukturalnej są kosztowne. Nieuzasadnione sprawdzanie, choć przyzwoite rozwiązanie tymczasowe, jest również zasadniczo cieknącą abstrakcją, która może powodować niepotrzebną złożoność aplikacji.

Dlaczego? Usuwanie zanieczyszczeń musi być przeprowadzane za każdym razem, gdy dane mają ulec zmianie. Po prostu nie ma solidnego sposobu na to, a każde z nich ma poważne wady (np.sprawdzenie przedziału odpytywania może stwarzać ryzyko pojawienia się artefaktów wizualnych i warunków wyścigu między problemami z kodem). Dokładne sprawdzanie wymaga też globalnego rejestru obserwacji, co pozwala uniknąć wycieku pamięci i kosztów utraty danych O.o().

Przyjrzyjmy się kilku liczbom.

Poniższe testy porównawcze (dostępne na GitHub) umożliwiają porównanie brudnego sprawdzania i O.o(). Mają one strukturę wykresów: zaobserwowane-obiekty-zestawu-ilość-ilości zmian. Ogólny wynik jest taki, że wydajność kontroli brudnej jest algorytmicznie proporcjonalna do liczby obserwowanych obiektów, a wydajność O.o() jest proporcjonalna do liczby mutacji.

brudne sprawdzanie

Brutalne sprawdzanie skuteczności

Chrome z włączoną funkcją Object.observe()

Obserwowanie skuteczności

Polyfilling Object.observe()

Świetnie – więc funkcja O.o() można używać w Chrome 36, ale co z korzystaniem z niej w innych przeglądarkach? Chętnie Ci pomożemy. Opracowany przez Polymer kod Observe-JS to kod polyfill dla O.o(), który korzysta z natywnej implementacji (jeśli jest dostępna), ale w przeciwnym razie stosuje ją do kodu polyfill i dodaje do niej przydatny cukier. Oferuje zagregowany widok świata, który podsumowuje zmiany i prezentuje to, co się zmieniło. Dwie niezwykle ważne aspekty, jakie daje, to:

  1. Możesz obserwować ścieżki. Oznacza to, że chcesz obserwować obiekt „foo.bar.baz” i informuje Cię, kiedy zmieniła się wartość na tej ścieżce. Jeśli ścieżka jest nieosiągalna, traktuje wartość jako niezdefiniowaną.

Przykład obserwacji wartości na ścieżce z danego obiektu:

var obj = { foo: { bar: 'baz' } };

var observer = new PathObserver(obj, 'foo.bar');
observer.open(function(newValue, oldValue) {
  // respond to obj.foo.bar having changed value.
});
  1. Poinformuje Cię o sklejeniach tablicy. Wycinki tablicy to w zasadzie minimalny zestaw operacji łączenia, które musisz wykonać na tablicy w celu przekształcenia starej wersji tablicy w jej nową wersję. To jest typ przekształcenia lub inny widok tablicy. To minimalna nakład pracy, jaką musisz wykonać, aby przejść ze starego stanu do nowego.

Przykład raportowania zmian w tablicy w postaci minimalnego zbioru segmentów:

var arr = [0, 1, 2, 4];

var observer = new ArrayObserver(arr);
observer.open(function(splices) {
  // respond to changes to the elements of arr.
  splices.forEach(function(splice) {
    splice.index; // index position that the change occurred.
    splice.removed; // an array of values representing the sequence of elements which were removed
    splice.addedCount; // the number of elements which were inserted.
  });
});

Platformy i funkcja Object.observe()

Jak już wspomnieliśmy, funkcja O.o() daje platformom i bibliotekom ogromną szansę na zwiększenie skuteczności wiązania danych w przeglądarkach, które obsługują tę funkcję.

Yehuda Katz i Erik Bryn z pracowników Ember potwierdzili, że dodanie obsługi funkcji O.o() jest uwzględnione w planach Ember. Misko Hervy z Angular napisała dokument projektowy dotyczący ulepszonego wykrywania zmian w Angular 2.0. Długoterminowym podejściem będzie wykorzystanie Object.observe() po udostępnieniu stabilnej wersji Chrome, a do tego czasu na korzystanie z Watchtower.js – własnego sposobu wykrywania zmian. Niesamowicie ekscytujące.

Podsumowanie

O.o() to rozbudowany dodatek do platformy internetowej, z którego możesz korzystać już dziś.

Mamy nadzieję, że z czasem funkcja ta będzie dostępna w większej liczbie przeglądarek, co pozwoli platformom JavaScriptu na poprawę wydajności dzięki dostępowi do natywnych funkcji obserwacji obiektów. Użytkownicy kierujący reklamy na Chrome powinni mieć możliwość korzystania z funkcji O.o() w Chrome 36 (i nowszych wersjach). Ta funkcja powinna być też dostępna w przyszłej wersji Opery.

Porozmawiaj więc z autorami platform JavaScript na temat Object.observe() i o tym, jak planują wykorzystać tę technologię do zwiększenia wydajności wiązania danych w Twoich aplikacjach. Przed Tobą wiele ekscytujących czasów!

Zasoby

Dziękujemy Rafaelowi Weinsteinowi, Jake Archibald, Ericowi Bidelman, Paulowi Kinlanowi i Vivian Cromwell za ich opinie i recenzje.