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

Addy Osmani
Addy Osmani

Wprowadzenie

Nadchodzi rewolucja. Wprowadziliśmy do niego nowy dodatek JavaScript, który zmieni wszystko, co wiesz o wiązaniu danych. Zmieni się też liczba Twoich bibliotek MVC, które podchodzą do obserwacji modeli edycji i aktualizacji. Chcesz poprawić skuteczność aplikacji, które umożliwiają obserwację nieruchomości?

Dobrze. Z przyjemnością informujemy, że aplikacja Object.observe() jest już dostępna w stabilnej wersji Chrome 36. [WOOOO. THE CROWD GOES WILD].

Object.observe(), która jest częścią przyszłego standardu ECMAScript, to metoda asynchronicznego obserwowania zmian w obiektach JavaScriptu bez potrzeby korzystania z osobnej biblioteki. Umożliwia on obserwatorowi otrzymywanie uporządkowanej czasowo sekwencji rekordów zmian, które opisują zestaw zmian wprowadzonych 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);
    });

});

Każda zmiana jest zgłaszana:

Zmiana została zgłoszona.

Za pomocą Object.observe() (lubię nazywane O.o() lub Oooooooo) można wdrożyć dwukierunkowe wiązanie danych bez konieczności tworzenia platformy.

Nie oznacza to jednak, że nie należy jej używać. W dużych projektach ze skomplikowaną logiką biznesową bezcenne są sprawdzone rozwiązania, które warto stosować. Ułatwiają oni orientację nowym deweloperom, wymagają mniej konserwacji kodu i określają wzorce wykonywania typowych zadań. Gdy ich nie potrzebujesz, możesz używać mniejszych i bardziej szczegółowych bibliotek, takich jak Polymer (które już korzystają z O.o()).

Nawet jeśli intensywnie korzystasz z frameworku lub biblioteki MV*, O.o() może zapewnić Ci znaczną poprawę wydajności dzięki szybszemu i prostszemu wdrożeniu przy zachowaniu tego samego interfejsu API. Na przykład w zeszłym roku Angular wykazało, że w analizie porównawczej, w której zmiany wprowadzane w modelu były wprowadzane, sprawdzanie zabrudzeń trwało 40 ms na aktualizację, a O.o() trwało 1–2 ms (poprawa 20–40 razy szybciej).

Powiązanie danych bez konieczności stosowania skomplikowanego kodu oznacza też, że nie musisz już przeprowadzać ankiety w celu sprawdzania zmian, co przedłuża czas pracy baterii.

Jeśli jesteś już przekonany/a do funkcji O.o(), przejdź do sekcji wprowadzającej lub dowiedz się więcej o problemach, które rozwiązuje.

Co chcemy obserwować?

Gdy mówimy o obserwowaniu danych, mamy na myśli zwracanie uwagi na określone typy zmian:

  • Zmiany w surowych obiektach JavaScriptu
  • Gdy właściwości są dodawane, zmieniane lub usuwane
  • gdy tablice zawierają elementy, które są w nich wstawiane lub usuwane;
  • Zmiany w prototypie obiektu

Znaczenie powiązań danych

Powiązanie danych zaczyna nabrać znaczenia, gdy zależy Ci na rozdzieleniu kontroli nad widokiem modelu. HTML to świetny mechanizm deklaratywny, ale jest całkowicie statyczny. Najlepiej po prostu zadeklarować związek między danymi a DOM, a następnie aktualizować DOM. Dzięki temu możesz oszczędzić sporo czasu na pisaniu powtarzającego się kodu, który tylko wysyła dane do i z DOM między stanem wewnętrznym aplikacji a serwerem.

Wiązanie danych jest szczególnie przydatne, gdy masz złożony interfejs użytkownika, w którym musisz nawiązywać relacje między wieloma właściwościami w modelach danych z wieloma elementami w widokach. Jest to dość powszechne w przypadku aplikacji jednostronicowych, które obecnie tworzymy.

Tworząc sposób na natywną obserwację danych w przeglądarce, umożliwiamy platformom JavaScript (i małym napisaniem przez Ciebie niewielkich bibliotek narzędziowych) sposób obserwowania zmian w danych modeli bez konieczności polegania na niektórych z powolnych ataków, z których korzysta obecnie świat.

Jak wygląda dzisiaj świat

Sprawdzanie zabrudzenia

Gdzie wcześniej spotykaliśmy się z pojęciem „wiązania danych”? Jeśli do tworzenia aplikacji internetowych (np.Angular, Knockout) używasz nowoczesnej biblioteki MV*, prawdopodobnie wiesz już, jak wiązać dane modelu z DOM. Oto przykład aplikacji z listą telefonów, w której powiązujemy wartość każdego telefonu w tablicy phones (zdefiniowanej w JavaScript) z elementem listy, aby 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 JavaScript dla 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 dane modelu podstawowego ulegną zmianie, nasza lista w DOM zostanie zaktualizowana. Jak to osiąga Angular? W tle wykonuje się coś takiego,

Sprawdzanie nieczystości

Podstawowa idea sprawdzania zmian polega na tym, że w każdej chwili, gdy dane mogą ulec zmianie, biblioteka musi sprawdzić, czy tak się stało, za pomocą cyklu zmian lub skrótu. W Angular cykl skrótu identyfikuje wszystkie zarejestrowane wyrażenia, które mają być obserwowane w celu sprawdzenia, czy nastąpiła zmiana. Wiedza o poprzednich wartościach modelu i jeśli ulegną zmianie, wywoływane jest zdarzenie zmiany. Główną zaletą dla programisty jest możliwość korzystania z surowych danych obiektów JavaScript, które są przyjemne w użyciu i dobrze się komponują. Minusem jest to, że ma ono nieprawidłowe działanie algorytmiczne i może być bardzo drogie.

Brudne sprawdzanie.

Koszty tej operacji są proporcjonalne do łącznej liczby obserwowanych obiektów. Może być konieczne wykonanie wielu niestandardowych kontroli. Może też być potrzebny sposób na rozpoczęcie sprawdzania, jeśli dane mogły się zmienić. Istnieje wiele sprytnych sztuczek, które można wykorzystać do tego celu. Nie jest jasne, czy uda się to osiągnąć w przyszłości.

Ekosystem internetowy powinien mieć więcej możliwości wprowadzania innowacji i rozwijania własnych mechanizmów deklaratywnych, np.

  • Systemy modeli oparte na ograniczeniach
  • systemy automatycznego przechowywania (np. przechowywanie zmian w IndexedDB lub localStorage);
  • Obiekty kontenera (Ember, Backbone)

W przypadku obiektów Container platforma tworzy obiekty, które wewnątrz zawierają dane. Mają dostęp do danych i mogą rejestrować to, co ustawiasz lub pobierasz, oraz przesyłać je wewnętrznie. To działa. Jest on stosunkowo wydajny i działa dobrze w ramach algorytmu. Poniżej znajdziesz przykład obiektów kontenera korzystających z Ember:

// 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

Koszt wykrycia zmian jest proporcjonalny do liczby zmienionych elementów. Innym problemem jest korzystanie z obiektu tego innego rodzaju. Ogólnie rzecz biorąc, musisz przekształcić dane otrzymywane z serwera w takie obiekty, aby były one widoczne.

Nie jest ona szczególnie dobrze dopasowana do istniejącego kodu JavaScript, ponieważ większość kodu zakłada, że może on działać na danych nieprzetworzonych. Nie dotyczy to wyspecjalizowanych obiektów.

Introducing Object.observe()

Chcielibyśmy połączyć zalety obu tych rozwiązań – mieć możliwość obserwowania danych z obsługą obiektów danych nieprzetworzonych (zwykłych obiektów JavaScript), jeśli zdecydujemy się na to, oraz bez konieczności ciągłego sprawdzania wszystkiego. Coś, co działa dobrze algorytmicznie. coś, co dobrze się składa i jest wbudowane w platformę. To piękno tego, co Object.observe() oferuje.

Umożliwia obserwowanie obiektu, modyfikowanie jego właściwości i wyświetlanie raportu o zmianach. Ale dosyć już tej teorii, przyjrzyjmy się kodowi!

Object.observe()

Obiekt.observe() i Obiekt.unobserve()

Załóżmy, że mamy prosty obiekt JavaScript reprezentujący model:

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

Możemy wtedy określić wywołanie zwrotne za każdym razem, gdy w obiekcie zostaną wprowadzone mutacje (zmiany):

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
  });
}

Możemy następnie obserwować te zmiany przy użyciu funkcji O.o(), przekazując obiekt jako pierwszy argument, a wywołanie zwrotne jako drugi:

Object.observe(todoModel, observer);

Zacznijmy od zmian w obiekcie modelu Todos:

todoModel.label = 'Buy some more milk';

W konsoli znajdziesz przydatne informacje. Wiemy, która właściwość się zmieniła, jak zmieniła się jej wartość i jaka jest jej nowa wartość.

Raport konsoli

Super! Do widzenia, sprawdzanie nieczystości! Napis na nagrobku powinien być wyryty czcionką Comic Sans. Zmieńmy inną usługę. Tym razem completeBy:

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

Jak widać, ponownie otrzymaliśmy raport o zmianach:

Raport o zmianach.

Świetnie! Co się stanie, jeśli zdecydujemy się usunąć z obiektu właściwość „ukończono”:

delete todoModel.completed;
Gotowe

Jak widać, raport o zwróconych zmianach zawiera informacje o usunięciu. Zgodnie z oczekiwaniami nowa wartość właściwości jest teraz niezdefiniowana. Wiemy już, że można dowiedzieć się, kiedy usługi zostały dodane. Gdy zostały usunięte. Zasadniczo zbiór właściwości obiektu („nowy”, „usunięty”, „przekonfigurowany”) i zmiana jego prototypu (proto).

Jak w przypadku każdego systemu obserwacji, istnieje też metoda na zaprzestanie nasłuchiwania zmian. W tym przypadku jest to Object.unobserve(), który ma taki sam podpis jak O.o(), ale może być wywoływany w ten sposób:

Object.unobserve(todoModel, observer);

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

Mutacje

Określanie zmian zainteresowań

Omówiliśmy podstawy otrzymywania listy zmian w obserwowanym obiekcie. Co zrobić, jeśli interesuje Cię tylko część zmian wprowadzonych w obiekcie, a nie wszystkie? Każdemu przyda się filtr spamu. Obserwatorzy mogą określić tylko te typy zmian, o których chcą otrzymywać powiadomienia za pomocą listy akceptacji. Można to określić za pomocą trzeciego argumentu funkcji O.o() w następujący sposób:

Object.observe(obj, callback, optAcceptList)

Oto przykład:

// 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 usuniemy etykietę, zauważysz, że tego typu zmiany są zgłaszane:

delete todoModel.label;

Jeśli nie określisz listy typów akceptacji do O.o(), domyślnie będą używane „wewnętrzne” typy zmian obiektów (add, update, delete, reconfigure, preventExtensions (gdy obiekt staje się niemożliwy do rozszerzenia)).

Powiadomienia

O.o() udostępnia też funkcję powiadomień. Nie są to te irytujące reklamy, które pojawiają się na telefonie, ale raczej przydatne informacje. Powiadomienia są podobne do obserwatorów mutacji. Mają miejsce na koniec mikrozadań. W kontekście przeglądarki prawie zawsze będzie to koniec bieżącego modułu obsługi zdarzeń.

Dobrze wyczuwa się moment, w którym kończy się jedna jednostka, a obserwatorzy mogą zająć się swoimi sprawami. To świetny turowy model przetwarzania.

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

Powiadomienia

Przyjrzyjmy się przykładowi użycia funkcji powiadomień w praktyce do definiowania niestandardowych powiadomień w przypadku pobierania lub ustawiania właściwości obiektu. Obserwuj komentarze 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 Powiadomienia

Tutaj raportujemy, gdy zmienia się wartość właściwości danych („aktualizacja”). Wszystkie inne ustawienia zgłaszane przez implementację obiektu (notifier.notifyChange()).

Nasze wieloletnie doświadczenie w zakresie platformy internetowej nauczyło nas, że podejście synchroniczne jest tym, od czego należy zacząć, ponieważ jest ono najprostsze do zrozumienia. Problem polega na tym, że tworzy on niebezpieczny model przetwarzania. Gdy piszesz kod i mówisz, żeby zaktualizować właściwość obiektu, nie chcesz, aby w sytuacji, w której po zaktualizowaniu właściwości tego obiektu byłby on zapraszany do wykonywania dowolnych działań. Unieważnianie założeń podczas pracy w środku funkcji nie jest idealnym rozwiązaniem.

Jeśli jesteś obserwatorem, nie chcesz być prawdopodobnie wywoływany, gdy ktoś jest w trakcie jakiegoś zadania. Gdyby nie było, gdyby trzeba było przychodzić do pracy nad niespójnym stanem świata, W końcu będzie sprawdzał znacznie więcej błędów. Próba tolerowania większej liczby złych sytuacji jest trudna do opracowania. Interfejs asynchroniczny jest trudniejszy w użyciu, ale w ogóle jest lepszym modelem.

Rozwiązaniem tego problemu są rekordy zmian syntetycznych.

Rekordy zmian syntetycznych

Jeśli chcesz mieć akcesory lub właściwości obliczeniowe, Twoim obowiązkiem jest powiadamianie o zmianach tych wartości. To trochę dodatkowej pracy, ale jest to funkcja tego mechanizmu, która ma być traktowana priorytetowo. Powiadomienia będą wysyłane razem z resztą powiadomień z podstawowych obiektów danych. Na podstawie usług danych.

Syntetyczne rekordy zmian

Obserwowanie funkcji dostępu i obliczanych właściwości można rozwiązać za pomocą notifier.notify – kolejnej części O.o(). Większość systemów obserwacji wymaga pewnej formy obserwowania wartości pochodzenia. Możesz to zrobić na wiele sposobów. O.o nie ocenia „właściwego” sposobu. Obliczane właściwości powinny być metodami dostępu, które powiadamią o zmianach wewnętrznego (prywatnego) stanu.

Deweloperzy powinni oczekiwać, że biblioteki ułatwią im tworzenie powiadomień i różnych metod obliczania właściwości (oraz zmniejszą ilość kodu stałego).

Przygotujmy kolejny przykład, czyli klasę koła. Chodzi o to, że mamy okrąg i właściwość radius. W tym przypadku promień jest elementem dostępu, a gdy jego wartość się zmieni, sam powiadomi o tym, że wartość się zmieniła. Zostanie ona przesłana wraz ze wszystkimi innymi zmianami wprowadzonymi w tym obiekcie lub dowolnym innym obiekcie. Jeśli wdrażasz obiekt, który ma mieć właściwości syntetyczne lub obliczone, musisz wybrać strategię, która to umożliwi. Gdy to zrobisz, będzie to pasować do całego systemu.

Przejdź dalej, aby zobaczyć, jak to działa w Narzędziach dla programistów.

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 rekordów zmian syntetycznych

Właściwości funkcji dostępu

Krótka uwaga na temat właściwości funkcji dostępu. Wspomniliśmy wcześniej, że w przypadku właściwości danych widoczne są tylko zmiany wartości. Nie dotyczy właściwości obliczeniowych ani metod dostępu. Dzieje się tak, ponieważ JavaScript nie ma w zasadzie pojęcia zmiany wartości w przystawnikach. Metoda dostępu to tylko zbiór funkcji.

Jeśli przypiszesz do funkcji dostępowej JavaScript, wywoła ona tę funkcję i z jej punktu widzenia nic się nie zmieni. Po prostu umożliwiło uruchomienie kodu.

Problem polega na tym, że semantycznie możemy spojrzeć na przypisanie powyżej do wartości - 5. Powinniśmy wiedzieć, co się tutaj stało. To nierozwiązywalny problem. Ten przykład pokazuje, dlaczego. Żaden system nie jest w stanie określić, co oznacza ten kod, ponieważ może to być dowolny kod. W tym przypadku może zrobić wszystko, co chce. Aktualizuje wartość przy każdym jej otwarciu, więc pytanie, czy ta zmiana nie ma sensu,

Obserwowanie wielu obiektów z 1 wywołaniem zwrotnym

Innym wzorcem możliwym do zastosowania w przypadku funkcji O.o() jest pojedynczy obserwator wywołania zwrotnego. Dzięki temu jedno wywołanie zwrotne może pełnić funkcję „obserwatora” dla wielu różnych obiektów. W ramach wywołania zwrotnego zostanie przesłany pełny zestaw zmian dla wszystkich obserwowanych obiektów „na końcu mikrozadania” (zauważ podobieństwo do obserwatorów mutacji).

Obserwowanie wielu obiektów za pomocą jednego wywołania zwrotnego

Zmiany na dużą skalę

A może pracujesz nad niezwykle dużą aplikacją i regularnie musisz wprowadzać zmiany na dużą skalę. Obiekty mogą chcieć opisywać większe zmiany semantyczne, które wpłyną na wiele właściwości w bardziej zwarty sposób (zamiast rozsyłać mnóstwo zmian właściwości).

Pomaga w tym funkcja O.o(), wykorzystując w tym celu 2 konkretne narzędzia: notifier.performChange() i notifier.notify(), które już wcześniej wprowadziliśmy.

Duże zmiany

Przyjrzyjmy się temu na przykładzie opisywania zmian na dużą skalę, w którym definiujemy obiekt Thingy za pomocą kilku narzędzi matematycznych (multiply, increment, incrementAndMultiply). Każdorazowy użytek z utility informuje system, że zbiór prac obejmuje 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 określamy 2 obserwatorów dla naszego obiektu: jeden jako zbiór uwzględniający zmiany i drugi, który przekazuje informacje tylko 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ąć bawić się tym kodem. Zdefiniujmy nowy element Thingy:

var thingy = new Thingy(2, 4);

Obserwuj je, a potem wprowadź zmiany. Ale super. TAK I tyle rzeczy!

// 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 }
Zmiany na dużą skalę

Wszystko, co znajduje się w ramach funkcji „perform function”, jest uważane za działanie „big-change”. Obserwatorzy, którzy akceptują „big-change”, otrzymają tylko rekord „big-change”. Obserwatorzy, którzy nie otrzymają podstawowych zmian wynikających z pracy, która wykonała „funkcję”.

Obserwowanie tablic

Rozmawialiśmy już o obserwowaniu zmian w obiektach, ale co z tablicami? Świetne pytanie. Gdy ktoś mówi mi „Świetne pytanie”. Nigdy nie słyszę ich odpowiedzi, bo chcę sobie pogratulować, że zadałam tak świetne pytanie, ale się denerwuję. Mamy też nowe metody pracy z tablicami.

Array.observe() to metoda, która traktuje duże zmiany w sobie – np. splice, unshift lub cokolwiek, co pośrednio zmienia długość – jako rekord zmiany „splice”. Wewnętrznie jest to notifier.performChange("splice",...).

Oto przykład, w którym obserwujemy model „array” i w podobny sposób otrzymujemy listę zmian w przypadku jakichkolwiek zmian w podstawowych danych:

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

Wyniki

O.o() można traktować jak pamięć podręczną do odczytu. Pamięć podręczna jest dobrym wyborem, gdy (według znaczenia):

  1. Częstotliwość odczytów przeważa nad częstotliwością zapisów.
  2. Możesz utworzyć pamięć podręczną, która wymaga stałej pracy wymaganej podczas zapisu w celu zwiększenia wydajności algorytmicznej podczas odczytów.
  3. Ciągłe spowolnienie zapisu jest akceptowalne.

Funkcja O.o() jest przeznaczona do takich zastosowań jak 1).

Sprawdzenie, czy nie jest brudne, wymaga przechowywania kopii wszystkich obserwowanych danych. Oznacza to, że pozyskanie danych strukturalnych, które jest niedostępne w przypadku O.o(), nie jest możliwe w przypadku funkcji O.o(). Dobrym rozwiązaniem, które pozwala uniknąć przerw w działaniu, jest też przeciekająca abstrakcja i może prowadzić do niepotrzebnej złożoności aplikacji.

Dlaczego? Sprawdzanie niekompletnych danych musi być wykonywane za każdym razem, gdy dane mogą ulec zmianie. Po prostu nie ma bardzo solidnego sposobu na to, a każde podejście do niego ma poważne wady (np.sprawdzenie interwału sondowania może narazić na szwank wizualny i warunków wyścigu między potencjalnymi problemami dotyczącymi kodu). Sprawdzanie nieczystości wymaga też globalnego rejestru obserwatorów, co powoduje ryzyko wycieku pamięci i wysokich kosztów dekompilacji, których można uniknąć za pomocą funkcji O.o().

Przyjrzyjmy się liczbom.

Testy porównawcze (dostępne na GitHubzie) umożliwiają porównanie sprawdzania stanu z funkcją O.o(). Są one ustrukturyzowane jako wykresy zbioru obserwowanych obiektów w zależności od liczby mutacji. Ogólnie rzecz biorąc, wydajność sprawdzania nieczystości jest proporcjonalna do liczby obserwowanych obiektów, a wydajność funkcji O.o() jest proporcjonalna do liczby wprowadzonych mutacji.

Sprawdzanie niespójności

Skuteczność sprawdzania niespójności

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

Sprawdzaj wyniki

Wypełnianie obiektu za pomocą funkcji Object.observe()

Świetnie – funkcja O.o() może być używana w Chrome 36, ale co z używaniem jej w innych przeglądarkach? Chętnie Ci pomożemy. Observe-JS to biblioteka polyfill dla O.o(), która korzysta z własnej implementacji, jeśli jest ona dostępna, a w przeciwnym razie tworzy ją i dodaje do niej przydatne funkcje. Udostępnia ona zbiorcze informacje o świecie, które podsumowują zmiany i tworzą raport o tym, co się zmieniło. Widoczne są w nim 2 rzeczy, które mają duże znaczenie:

  1. Możesz obserwować ścieżki. Oznacza to, że możesz określić, że chcesz obserwować „foo.bar.baz” w danym obiekcie, a system poinformuje Cię, gdy wartość na tej ścieżce ulegnie zmianie. Jeśli ścieżka jest niedostępna, wartość jest uważana za nieokreśloną.

Przykład obserwacji wartości na ścieżce od 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. Wyświetli się opis połączeń tablic. Wycinki tablic to zasadniczo minimalny zestaw operacji łączenia, które musisz wykonać na tablicy, aby przekształcić starą wersję tablicy w jej nową wersję. Jest to rodzaj przekształcenia lub inny widok tablicy. To minimalna ilość pracy, jaką musisz wykonać, aby przejść ze starego stanu do nowego.

Przykład raportowania zmian w tablicy jako minimalnego zbioru przecięć:

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.
  });
});

Frameworki i Object.observe()

Jak już wspomnieliśmy, funkcja O.o() daje frameworkom i bibliotekom ogromne możliwości poprawy wydajności powiązań danych w przeglądarkach, które obsługują tę funkcję.

Yehuda Katz i Erik Bryn z Ember potwierdzili, że dodanie obsługi funkcji O.o() jest w najbliższej perspektywie planowane przez Ember. Misko Hervy z Angular napisał dokument projektowy na temat ulepszonego wykrywania zmian w Angular 2.0. W dłuższej perspektywie firma będzie korzystać z metody Object.observe(), która trafi do stabilnej wersji Chrome, i wybiera Watchtower.js – do tego czasu własne podejście do wykrywania zmian. Ale czad.

Podsumowanie

O.o() to zaawansowane uzupełnienie platformy internetowej, z której możesz już korzystać.

Mamy nadzieję, że z czasem funkcja ta pojawi się w większej liczbie przeglądarek, co umożliwi platformom JavaScript zwiększoną wydajność dzięki dostępowi do natywnych funkcji obserwacji obiektów. Użytkownicy Chrome powinni mieć możliwość korzystania z O.o() w Chrome w wersji 36 (i nowszych). Ta funkcja powinna być także dostępna w przyszłej wersji przeglądarki Opera.

Porozmawiaj z twórcami frameworków JavaScript o Object.observe() i o tym, jak planują go używać do zwiększania wydajności powiązań danych w aplikacjach. Z pewnością czekają Cię ekscytujące chwile!

Zasoby

Z podziękowaniem Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan i Vivian Cromwell za opinie i opinie.