Datenbindungs-Umdrehungen mit Object.observe()

Einführung

Eine Revolution steht bevor. JavaScript wurde um eine neue Funktion erweitert, die alles verändert, was Sie bisher über die Datenbindung wussten. Außerdem ändert sich, wie viele Ihrer MVC-Bibliotheken Modelle auf Änderungen und Aktualisierungen prüfen. Sind Sie bereit für eine Leistungssteigerung bei Apps, bei denen die Beobachtung von Properties wichtig ist?

Okay. Okay. Ich kann Ihnen mitteilen, dass Object.observe() in der stabilen Version von Chrome 36 enthalten ist. [WOOOO. DIE MENGE JUBELT].

Object.observe(), Teil eines zukünftigen ECMAScript-Standards, ist eine Methode zum asynchronen Beobachten von Änderungen an JavaScript-Objekten – ohne dass eine separate Bibliothek erforderlich ist. Es ermöglicht einem Beobachter, eine zeitgeordnete Abfolge von Änderungseinträgen zu erhalten, die die Änderungen an einer Gruppe von beobachteten Objekten beschreiben.

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

});

Jede Änderung wird gemeldet:

Änderung gemeldet.

Mit Object.observe() (ich nenne es gern O.o() oder Oooooooo) können Sie die bidirektionale Datenbindung ohne Framework implementieren.

Das bedeutet aber nicht, dass Sie keine verwenden sollten. Bei großen Projekten mit komplexer Geschäftslogik sind Frameworks mit starker Meinung unverzichtbar und sollten weiterhin verwendet werden. Sie vereinfachen die Orientierung neuer Entwickler, erfordern weniger Codewartung und legen Muster für die Ausführung gängiger Aufgaben fest. Wenn Sie keine benötigen, können Sie kleinere, spezifischere Bibliotheken wie Polymer verwenden, die bereits O.o() nutzen.

Auch wenn Sie ein Framework oder eine MV*-Bibliothek häufig verwenden, kann O.o() die Leistung erheblich verbessern. Dabei wird die API beibehalten, aber die Implementierung ist schneller und einfacher. Letztes Jahr wurde in Angular festgestellt, dass bei einem Benchmark, bei dem Änderungen an einem Modell vorgenommen wurden, die Dirty-Check-Funktion 40 ms pro Aktualisierung benötigte und O.o() 1–2 ms pro Aktualisierung (eine Verbesserung von 20–40 mal schneller).

Da für die Datenbindung kein komplizierter Code erforderlich ist, müssen Sie auch nicht mehr nach Änderungen suchen. Das bedeutet eine längere Akkulaufzeit.

Wenn Sie schon von O.o() überzeugt sind, springen Sie zur Einführung in die Funktion. Andernfalls lesen Sie weiter, um mehr über die Probleme zu erfahren, die damit gelöst werden.

Was möchten wir beobachten?

Wenn wir von der Beobachtung von Daten sprechen, meinen wir in der Regel, dass wir ein Auge auf bestimmte Arten von Änderungen werfen:

  • Änderungen an Roh-JavaScript-Objekten
  • Wenn Properties hinzugefügt, geändert oder gelöscht werden
  • Wenn Elemente in Arrays eingefügt und daraus entfernt werden
  • Änderungen am Prototyp des Objekts

Die Bedeutung der Datenbindung

Die Datenbindung wird wichtig, wenn Sie die Trennung von Modell- und Ansichtssteuerung beachten möchten. HTML ist ein großartiger deklarativer Mechanismus, aber er ist völlig statisch. Idealerweise sollten Sie nur die Beziehung zwischen Ihren Daten und dem DOM deklarieren und das DOM auf dem neuesten Stand halten. So sparen Sie viel Zeit beim Schreiben von sich wiederholendem Code, der nur Daten zwischen dem DOM und dem internen Status Ihrer Anwendung oder dem Server sendet.

Die Datenbindung ist besonders nützlich, wenn Sie eine komplexe Benutzeroberfläche haben, bei der Sie Beziehungen zwischen mehreren Properties in Ihren Datenmodellen mit mehreren Elementen in Ihren Ansichten herstellen müssen. Das ist bei den heutigen Single-Page-Anwendungen ziemlich üblich.

Durch die native Beobachtung von Daten im Browser können JavaScript-Frameworks (und von Ihnen geschriebene kleine Dienstbibliotheken) Änderungen an Modelldaten beobachten, ohne auf einige der langsamen Hacks zurückgreifen zu müssen, die heute verwendet werden.

So sieht die Welt heute aus

Dirty-Checking

Wo haben Sie schon einmal Datenbindung gesehen? Wenn Sie eine moderne MV*-Bibliothek zum Erstellen Ihrer Webanwendungen verwenden (z. B. Angular oder Knockout), sind Sie wahrscheinlich daran gewöhnt, Modelldaten an das DOM zu binden. Zur Wiederholung: Hier ist ein Beispiel für eine Telefonliste, in der wir den Wert jedes Smartphones in einem phones-Array (in JavaScript definiert) an ein Listenelement binden, damit unsere Daten und die Benutzeroberfläche immer synchron sind:

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

und das JavaScript für den Controller:

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

Jedes Mal, wenn sich die zugrunde liegenden Modelldaten ändern, wird unsere Liste im DOM aktualisiert. Wie erreicht Angular dies? Hinter den Kulissen wird eine sogenannte Dirty-Check-Methode verwendet.

Dirty-Checking

Bei der schmutzigen Prüfung geht es darum, dass die Bibliothek jedes Mal, wenn sich Daten geändert haben könnten, über einen Digest- oder Änderungszyklus prüfen muss, ob sich die Daten tatsächlich geändert haben. Bei Angular werden in einem Digest-Zyklus alle registrierten Ausdrücke identifiziert, um zu prüfen, ob eine Änderung vorliegt. Sie kennt die vorherigen Werte eines Modells und wenn sich diese geändert haben, wird ein Änderungsereignis ausgelöst. Der Hauptvorteil für Entwickler besteht darin, dass sie rohe JavaScript-Objektdaten verwenden können, die sich gut verwenden und relativ gut zusammenstellen lassen. Der Nachteil ist, dass das algorithmische Verhalten schlecht ist und die Methode potenziell sehr teuer ist.

Dirty Checking

Die Kosten dieses Vorgangs sind proportional zur Gesamtzahl der beobachteten Objekte. Ich muss möglicherweise viel schmutziges Prüfen durchführen. Möglicherweise ist auch eine Möglichkeit erforderlich, die schmutzige Prüfung auszulösen, wenn sich Daten möglicherweise geändert haben. Es gibt viele clevere Tricks, die Frameworks dafür verwenden. Es ist unklar, ob das jemals perfekt sein wird.

Das Web sollte mehr Möglichkeiten haben, eigene deklarative Mechanismen zu entwickeln und zu verbessern, z.B.

  • Einschränkungsbasierte Modellsysteme
  • Systeme mit automatischer Persistenz (z. B. Persistenz von Änderungen in IndexedDB oder localStorage)
  • Containerobjekte (Ember, Backbone)

Mit Container-Objekten werden Objekte erstellt, die die Daten enthalten. Sie haben Zugriff auf die Daten und können erfassen, was Sie festlegen oder abrufen, und intern übertragen. Das funktioniert gut. Er ist relativ leistungsstark und hat ein gutes algorithmisches Verhalten. Unten finden Sie ein Beispiel für Containerobjekte mit 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

Die Kosten für die Ermittlung der Änderungen sind proportional zur Anzahl der Änderungen. Ein weiteres Problem ist, dass Sie jetzt eine andere Art von Objekt verwenden. Im Allgemeinen müssen Sie die Daten, die Sie vom Server erhalten, in diese Objekte umwandeln, damit sie beobachtet werden können.

Das passt nicht besonders gut zu vorhandenem JS-Code, da in den meisten Fällen davon ausgegangen wird, dass er mit Rohdaten arbeiten kann. Nicht für diese speziellen Objekttypen.

Introducing Object.observe()

Idealerweise möchten wir das Beste aus beiden Welten: eine Möglichkeit, Daten mit Unterstützung für Rohdatenobjekte (normale JavaScript-Objekte) zu beobachten, wenn wir das möchten, und ohne dass wir ständig alles auf Änderungen prüfen müssen. Etwas mit gutem algorithmischem Verhalten. Etwas, das sich gut zusammenstellen lässt und in die Plattform integriert ist. Das ist das Schöne an Object.observe().

So können wir ein Objekt beobachten, Eigenschaften ändern und den Änderungsbericht mit den entsprechenden Informationen aufrufen. Aber genug mit der Theorie. Sehen wir uns ein paar Codebeispiele an.

Object.observe()

Object.observe() und Object.unobserve()

Angenommen, wir haben ein einfaches JavaScript-Objekt, das ein Modell darstellt:

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

Wir können dann einen Callback für jede Mutation (Änderung) an dem Objekt angeben:

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

Diese Änderungen können wir dann mit O.o() beobachten, indem wir das Objekt als erstes Argument und den Rückruf als zweites Argument übergeben:

Object.observe(todoModel, observer);

Nehmen wir einige Änderungen an unserem Todos-Modellobjekt vor:

todoModel.label = 'Buy some more milk';

In der Konsole sehen wir einige nützliche Informationen. Wir wissen, welche Property sich geändert hat, wie sie sich geändert hat und wie der neue Wert lautet.

Console-Bericht

Sehr gut! Adieu, Dirty-Checking! Die Inschrift auf deinem Grabstein sollte in Comic Sans geschrieben sein. Ändern wir eine andere Property. Dieses Mal completeBy:

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

Wie Sie sehen, erhalten wir wieder einen Änderungsbericht:

Bericht ändern

Sehr gut. Was passiert, wenn wir die Property „completed“ aus unserem Objekt löschen?

delete todoModel.completed;
Abgeschlossen

Wie Sie sehen, enthält der Bericht zu den Änderungen Informationen zum Löschen. Wie erwartet ist der neue Wert der Property jetzt nicht definiert. Sie wissen jetzt, wie Sie herausfinden können, wann Unterkünfte hinzugefügt wurden. Wenn sie gelöscht wurden. Im Grunde geht es um die Eigenschaften eines Objekts („neu“, „gelöscht“, „neu konfiguriert“) und die Änderung des Prototyps (proto).

Wie bei jedem Beobachtungssystem gibt es auch eine Methode, um das Auschecken von Änderungen zu beenden. In diesem Fall ist es Object.unobserve(), das dieselbe Signatur wie O.o() hat, aber so aufgerufen werden kann:

Object.unobserve(todoModel, observer);

Wie unten zu sehen ist, werden nach der Ausführung keine Änderungsdatensätze mehr zurückgegeben, wenn am Objekt Änderungen vorgenommen werden.

Mutationen

Änderungen des Interesses angeben

Wir haben uns also mit den Grundlagen dafür beschäftigt, wie Sie eine Liste der Änderungen an einem beobachteten Objekt abrufen. Was ist, wenn Sie nur an einer Teilmenge der Änderungen an einem Objekt interessiert sind? Jeder braucht einen Spamfilter. Beobachter können über eine Zulassungsliste nur die Arten von Änderungen angeben, über die sie informiert werden möchten. Dies kann mit dem dritten Argument für O.o() so angegeben werden:

Object.observe(obj, callback, optAcceptList)

Sehen wir uns ein Beispiel an:

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

Wenn wir das Label jetzt jedoch löschen, wird diese Art von Änderung gemeldet:

delete todoModel.label;

Wenn Sie für O.o() keine Liste von Accept-Typen angeben, werden standardmäßig die „intrinsischen“ Objektänderungstypen verwendet (add, update, delete, reconfigure, preventExtensions, wenn nicht beobachtbar ist, dass ein Objekt nicht mehr erweiterbar ist).

Benachrichtigungen

O.o() bietet auch Benachrichtigungen. Sie sind nicht mit den nervigen Benachrichtigungen auf einem Smartphone vergleichbar, sondern eher nützlich. Benachrichtigungen ähneln Mutationsbeobachtern. Sie werden am Ende der Mikroaufgabe angezeigt. Im Browserkontext ist das fast immer am Ende des aktuellen Ereignis-Handlers.

Das Timing ist gut, da in der Regel eine Arbeitseinheit abgeschlossen ist und die Beobachter jetzt ihre Arbeit erledigen können. Es ist ein schönes rundenbasiertes Verarbeitungsmodell.

Der Workflow für die Verwendung eines Benachrichtigungstools sieht in etwa so aus:

Benachrichtigungen

Sehen wir uns ein Beispiel an, wie Notifierer in der Praxis verwendet werden können, um benutzerdefinierte Benachrichtigungen für den Abruf oder die Festlegung von Eigenschaften eines Objekts zu definieren. Hier findest du aktuelle Informationen:

// 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);
Benachrichtigungskonsole

Hier wird erfasst, wenn sich der Wert der Dateneigenschaften ändert („aktualisieren“). Alle anderen Informationen, die von der Implementierung des Objekts erfasst werden (notifier.notifyChange()).

Durch unsere langjährige Erfahrung mit der Webplattform haben wir gelernt, dass ein synchroner Ansatz das Erste ist, was Sie ausprobieren sollten, da er am einfachsten zu verstehen ist. Das Problem ist, dass dadurch ein grundlegend gefährliches Verarbeitungsmodell entsteht. Wenn Sie Code schreiben und beispielsweise die Property eines Objekts aktualisieren, sollten Sie nicht zulassen, dass durch das Aktualisieren der Property dieses Objekts beliebiger Code ausgeführt wird. Es ist nicht ideal, wenn Ihre Annahmen in der Mitte einer Funktion ungültig werden.

Wenn Sie Beobachter sind, sollten Sie idealerweise nicht angerufen werden, wenn jemand gerade etwas tut. Sie möchten nicht gebeten werden, an einem inkonsistenten Zustand der Welt zu arbeiten. Sie müssen viel mehr Fehler prüfen. Sie versuchen, viel mehr schlechte Situationen zu tolerieren. Im Allgemeinen ist es ein schwieriges Modell. Async ist schwieriger zu handhaben, aber am Ende ist es ein besseres Modell.

Die Lösung für dieses Problem sind synthetische Änderungseinträge.

Synthetische Änderungsdatensätze

Wenn Sie Accessoren oder berechnete Eigenschaften verwenden möchten, müssen Sie also dafür sorgen, dass diese Werte aktualisiert werden. Das ist zwar etwas mehr Aufwand, aber es ist eine Art Hauptfunktion dieses Mechanismus. Diese Benachrichtigungen werden zusammen mit den restlichen Benachrichtigungen von zugrunde liegenden Datenobjekten gesendet. Aus Dateneigenschaften

Synthetische Änderungsdatensätze

Das Beobachten von Accessoren und berechneten Eigenschaften kann mit notifier.notify gelöst werden, einem weiteren Teil von O.o(). Die meisten Beobachtungssysteme erfordern eine gewisse Form der Beobachtung abgeleiteter Werte. Dafür gibt es viele Möglichkeiten. O.o macht kein Urteil darüber, was die „richtige“ Art ist. Berechnete Eigenschaften sollten Zugriffsmethoden sein, die benachrichtigen, wenn sich der interne (private) Status ändert.

Webentwickler sollten davon ausgehen, dass Bibliotheken die Benachrichtigung und verschiedene Ansätze für berechnete Eigenschaften vereinfachen und den Boilerplate-Code reduzieren.

Stellen wir das nächste Beispiel ein, eine Kreisklasse. Wir haben hier einen Kreis mit einem Radius. In diesem Fall ist „radius“ ein Accessor. Wenn sich sein Wert ändert, wird er selbst benachrichtigt. Diese werden zusammen mit allen anderen Änderungen an diesem oder einem anderen Objekt gesendet. Wenn Sie ein Objekt implementieren, sollten Sie synthetische oder berechnete Properties verwenden oder eine Strategie für die Funktionsweise auswählen. Sobald Sie das getan haben, passt es in Ihr System als Ganzes.

Überspringen Sie den Code, um zu sehen, wie das in den Entwicklertools funktioniert.

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);
  })
}
Konsole für synthetische Änderungseinträge

Zugriffseigenschaften

Kurzer Hinweis zu Zugriffseigenschaften Wie bereits erwähnt, sind für Dateneigenschaften nur die Wertänderungen sichtbar. Nicht für berechnete Eigenschaften oder Accessors. Der Grund dafür ist, dass es in JavaScript keine Änderungen des Werts für Zugriffsmethoden gibt. Ein Accessor ist nur eine Sammlung von Funktionen.

Wenn Sie einem Zugriffsobjekt JavaScript zuweisen, wird die Funktion dort einfach aufgerufen und aus seiner Sicht hat sich nichts geändert. Es wurde lediglich Code ausgeführt.

Das Problem ist semantisch, wenn wir uns unsere oben genannte Zuweisung des Werts - 5 ansehen. Wir sollten herausfinden können, was hier passiert ist. Das ist eigentlich ein unlösbares Problem. Das Beispiel veranschaulicht das. Es gibt keine Möglichkeit, dass ein System weiß, was damit gemeint ist, da es sich um beliebigen Code handeln kann. In diesem Fall kann es beliebige Aktionen ausführen. Der Wert wird jedes Mal aktualisiert, wenn darauf zugegriffen wird. Daher macht es keinen Sinn, zu fragen, ob er sich geändert hat.

Mehrere Objekte mit einem Rückruf beobachten

Ein weiteres Muster, das mit O.o() möglich ist, ist der Begriff eines einzelnen Callback-Beobachters. So kann ein einzelner Rückruf als „Beobachter“ für viele verschiedene Objekte verwendet werden. Der Rückruf erhält am „Ende der Mikroaufgabe“ die vollständigen Änderungen an allen Objekten, die er beobachtet. Beachten Sie die Ähnlichkeit mit Mutation Observers.

Mehrere Objekte mit einem Rückruf beobachten

Große Änderungen

Vielleicht arbeiten Sie an einer wirklich großen App und müssen regelmäßig mit umfangreichen Änderungen arbeiten. Mit Objekten können Sie größere semantische Änderungen beschreiben, die sich auf viele Properties auswirken, auf kompaktere Weise (anstatt viele Property-Änderungen zu übertragen).

O.o() unterstützt Sie dabei mit zwei speziellen Dienstprogrammen: notifier.performChange() und notifier.notify(), die wir bereits kennengelernt haben.

Große Änderungen

Sehen wir uns an, wie sich groß angelegte Änderungen beschreiben lassen, indem wir ein Thingy-Objekt mit einigen mathematischen Dienstprogrammen (multiply, increment, incrementAndMultiply) definieren. Jedes Mal, wenn ein Dienstprogramm verwendet wird, wird dem System mitgeteilt, dass eine Arbeitssammlung eine bestimmte Art von Änderung umfasst.

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

Wir definieren dann zwei Beobachter für unser Objekt: einen, der alle Änderungen erfasst, und einen, der nur über bestimmte von uns definierte Accept-Typen (Thingy.INCREMENT, Thingy.MULTIPLY, Thingy.INCREMENT_AND_MULTIPLY) berichtet.

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

Wir können jetzt mit diesem Code spielen. Definieren wir ein neues Ding:

var thingy = new Thingy(2, 4);

Beobachten Sie das Ganze und nehmen Sie dann einige Änderungen vor. OMG, so lustig. SO viele Dinge!

// 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 }
Große Änderungen

Alles innerhalb der „perform function“ wird als Arbeit von „big-change“ betrachtet. Beobachter, die „big-change“ akzeptieren, erhalten nur den „big-change“-Eintrag. Beobachter, die dies nicht tun, erhalten die zugrunde liegenden Änderungen, die sich aus der Ausführung der Funktion ergeben.

Arrays beobachten

Wir haben schon darüber gesprochen, wie sich Änderungen an Objekten beobachten lassen. Aber was ist mit Arrays? Gute Frage. Wenn jemand zu mir sagt: „Gute Frage.“ Ich höre nie die Antwort, weil ich damit beschäftigt bin, mich selbst dafür zu beglückwünschen, dass ich eine so gute Frage gestellt habe. Aber ich schweife ab. Außerdem gibt es neue Methoden für die Arbeit mit Arrays.

Array.observe() ist eine Methode, die umfangreiche Änderungen an sich selbst behandelt, z. B. Splicing, Unshift oder alles, was die Länge implizit ändert, als Änderungsdatensatz vom Typ „Splice“. Intern wird notifier.performChange("splice",...) verwendet.

Hier ein Beispiel, in dem wir ein Modell-Array beobachten und bei Änderungen an den zugrunde liegenden Daten eine Liste der Änderungen zurückerhalten:

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';
Arrays beobachten

Leistung

Sie können sich die Auswirkungen von O.o() auf die Rechenleistung als Lesecache vorstellen. Im Allgemeinen ist ein Cache in folgenden Fällen eine gute Wahl:

  1. Die Häufigkeit der Lesevorgänge überwiegt die Häufigkeit der Schreibvorgänge.
  2. Sie können einen Cache erstellen, bei dem die konstante Arbeitsbelastung beim Schreiben durch eine algorithmisch bessere Leistung beim Lesen ausgeglichen wird.
  3. Die konstante Zeitverzögerung bei Schreibvorgängen ist akzeptabel.

O.o() ist für Anwendungsfälle wie 1) konzipiert.

Für die Dirty-Check-Methode muss eine Kopie aller beobachteten Daten aufbewahrt werden. Das bedeutet, dass durch die Dirty-Checking-Funktion strukturelle Speicherkosten entstehen, die bei O.o() nicht anfallen. Dirty-Checking ist zwar eine gute Notlösung, aber auch eine grundsätzlich unzureichende Abstraktion, die Anwendungen unnötig komplex machen kann.

Warum? Die schmutzige Prüfung muss immer ausgeführt werden, wenn sich Daten möglicherweise geändert haben. Es gibt einfach keine sehr robuste Möglichkeit, dies zu tun, und jeder Ansatz hat erhebliche Nachteile.Beispielsweise besteht bei der Prüfung eines Polling-Intervalls das Risiko von visuellen Artefakten und Race-Zuständen zwischen Codeproblemen. Für die schmutzige Prüfung ist außerdem eine globale Registrierung von Beobachtern erforderlich, was zu Speicherlecks und Deaktivierungskosten führt, die O.o() vermeidet.

Sehen wir uns einige Zahlen an.

Mit den folgenden Benchmark-Tests (verfügbar auf GitHub) können wir die Dirty-Checking-Methode mit O.o() vergleichen. Sie sind als Diagramme der Größe des beobachteten Objektsatzes im Vergleich zur Anzahl der Mutationen strukturiert. Das allgemeine Ergebnis ist, dass die Leistung der schmutzigen Überprüfung algorithmisch proportional zur Anzahl der beobachteten Objekte ist, während die Leistung von O.o() proportional zur Anzahl der durchgeführten Mutationen ist.

Dirty-Checking

Leistung der schmutzigen Prüfung

Chrome mit aktivierter Funktion „Object.observe()“

Leistung beobachten

Polyfilling Object.observe()

Super. O.o() kann also in Chrome 36 verwendet werden. Aber wie sieht es in anderen Browsern aus? Die beantworten wir Ihnen gern. Observe-JS von Polymer ist eine Polyfill für O.o(), die die native Implementierung verwendet, falls vorhanden, andernfalls aber eine Polyfill-Implementierung verwendet und einige nützliche Vereinfachungen enthält. Sie bietet eine globale Übersicht, in der Änderungen zusammengefasst und die Änderungen aufgeführt werden. Zwei wirklich nützliche Funktionen sind:

  1. Sie können Pfade beobachten. Sie können also festlegen, dass „foo.bar.baz“ eines bestimmten Objekts beobachtet werden soll. Sie werden dann benachrichtigt, wenn sich der Wert an diesem Pfad ändert. Wenn der Pfad nicht erreichbar ist, wird der Wert als nicht definiert betrachtet.

Beispiel für das Beobachten eines Werts an einem Pfad von einem bestimmten Objekt:

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. Sie erfahren dort mehr über Array-Splices. Array-Splices sind im Grunde die minimalen Splice-Vorgänge, die Sie an einem Array ausführen müssen, um die alte Version des Arrays in die neue Version des Arrays umzuwandeln. Dies ist eine Art von Transformation oder eine andere Ansicht des Arrays. Es ist der minimale Aufwand, der erforderlich ist, um vom alten Status zum neuen Status zu wechseln.

Beispiel für die Meldung von Änderungen an einem Array als minimaler Satz von Spleißungen:

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

Frameworks und Object.observe()

Wie bereits erwähnt, bietet O.o() Frameworks und Bibliotheken eine enorme Möglichkeit, die Leistung ihrer Datenbindung in Browsern zu verbessern, die die Funktion unterstützen.

Yehuda Katz und Erik Bryn von Ember haben bestätigt, dass die Unterstützung für O.o() in der nächsten Roadmap von Ember enthalten ist. Misko Hervy von Angular hat eine Designdokumentation zur verbesserten Änderungserkennung in Angular 2.0 verfasst. Langfristig wird Google Object.observe() nutzen, sobald es in der stabilen Chrome-Version verfügbar ist. Bis dahin wird Watchtower.js verwendet, der eigene Ansatz von Google zur Änderungserkennung. Das ist super spannend.

Ergebnisse

O.o() ist eine leistungsstarke Ergänzung der Webplattform, die Sie sofort verwenden können.

Wir hoffen, dass die Funktion mit der Zeit in mehr Browsern verfügbar sein wird, damit JavaScript-Frameworks von der Zugriffsmöglichkeit auf native Objekterkennungsfunktionen profitieren können. Nutzer, die ihre Anzeigen auf Chrome ausrichten, sollten O.o() in Chrome 36 und höher verwenden können. Die Funktion sollte auch in einer zukünftigen Opera-Version verfügbar sein.

Sprechen Sie also mit den Autoren von JavaScript-Frameworks über Object.observe() und wie sie es verwenden möchten, um die Leistung der Datenbindung in Ihren Apps zu verbessern. Es stehen auf jeden Fall spannende Zeiten bevor!

Ressourcen

Mit Dank an Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan und Vivian Cromwell für ihren Input und ihre Rezensionen.