Datenbindungs-Umdrehungen mit Object.observe()

Addy Osmani
Addy Osmani

Einführung

Eine Revolution steht bevor. Es gibt eine neue Ergänzung zu JavaScript, die alles ändern wird, was Sie über Datenbindung wissen. 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 Property-Beobachtung wichtig ist?

Okay. Okay. Ich freue mich, Ihnen mitteilen zu können, dass Object.observe() in der stabilen Version von Chrome 36 enthalten ist. [WOOOO. DIE MENGE JUBELT].

Object.observe() ist Teil eines zukünftigen ECMAScript-Standards und eine Methode zur asynchronen Beobachtung von Änderungen an JavaScript-Objekten, ohne dass eine separate Bibliothek erforderlich ist. Sie ermöglicht es einem Beobachter, eine zeitlich geordnete Sequenz von Änderungsdatensätzen zu erhalten, die die Änderungen beschreiben, die an einer Gruppe beobachteter Objekte vorgenommen wurden.

// 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 folgendermaßen gemeldet:

Änderung gemeldet.

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

Das heißt aber nicht, dass Sie sie nicht 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.

Selbst wenn Sie häufig ein Framework oder eine MV*-Bibliothek verwenden, bietet O.o() das Potenzial, einige gesunde Leistungsverbesserungen mit einer schnelleren, einfacheren Implementierung unter Beibehaltung derselben API zu erzielen. 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 O.o() bereits verkauft haben, fahren Sie mit der Einführung der Funktionen fort oder lesen Sie weiter, um mehr über die Probleme zu erfahren, die damit gelöst werden können.

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 JavaScript-Rohobjekten
  • 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 zunehmend wichtig, wenn die Trennung zwischen Modellansicht und Steuerung wichtig ist. HTML ist ein großartiger deklarativer Mechanismus, ist aber vollständig statisch. Im Idealfall legen Sie einfach nur die Beziehung zwischen Ihren Daten und dem DOM fest und halten das DOM auf dem neuesten Stand. Dies schafft Abhilfe und spart Ihnen viel Zeit beim Schreiben von wirklich sich wiederholendem Code, der nur Daten zwischen dem internen Status Ihrer Anwendung oder dem Server an das und vom DOM 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.

Wir bieten eine Möglichkeit, Daten nativ im Browser zu beobachten, und bieten JavaScript-Frameworks (und kleine Dienstprogrammbibliotheken, die Sie schreiben), um Änderungen an Modelldaten zu beobachten, ohne sich auf einige der weltweit genutzten langsamen Hacks zu verlassen.

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 Auffrischung ist hier ein Beispiel für eine Telefonlisten-App, bei der wir den Wert jedes Telefons 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-Prüfung durchgeführt.

Schmutzige Prüfung

Die Grundidee der Dirty-Checking-Methode besteht darin, dass die Bibliothek jedes Mal, wenn sich Daten geändert haben könnten, mithilfe eines Digest- oder Änderungszyklus überprüfen muss, ob sie sich geändert haben. Im Fall von Angular identifiziert ein Digest-Zyklus alle Ausdrücke, die für die Überwachung registriert sind, um zu sehen, ob es eine Änderung gibt. 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.

Schmutzige Prüfung.

Die Kosten dieses Vorgangs sind proportional zur Gesamtzahl der beobachteten Objekte. Ich muss möglicherweise viel schmutziges Prüfen durchführen. Möglicherweise benötigen Sie auch eine Möglichkeit, 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 dies 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)

Container sind Objekte, in denen ein Framework Objekte erstellt, die die Daten enthalten. Sie haben Zugriffsrechte für die Daten und können die von Ihnen festgelegten oder erhaltenen Daten erfassen und intern senden. Das funktioniert gut. Er ist relativ leistungsstark und hat ein gutes algorithmisches Verhalten. Ein Beispiel für Containerobjekte, die Ember verwenden, finden Sie unten:

// 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 diese 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 Codes davon ausgegangen wird, dass er mit Rohdaten arbeiten kann. Nicht für diese speziellen Arten von Objekten geeignet.

Introducing Object.observe()

Idealerweise wollen wir das Beste aus beiden Welten – eine Möglichkeit zur Beobachtung von Daten mit Unterstützung für Rohdatenobjekte (normale JavaScript-Objekte), wenn wir UND wählen, ohne ständig alles Dirty-Check durchzuführen. Etwas mit einem guten algorithmischen Verhalten. Etwas, das gut in die Plattform integriert ist. Das ist das Tolle an den Funktionen von Object.observe().

Damit können Sie ein Objekt beobachten, Eigenschaften ändern und einen Änderungsbericht zu den Änderungen einsehen. Aber genug mit der Theorie. Sehen wir uns ein paar Codebeispiele an.

Object.observe()

Object.observe() und Object.unobserve()

Nehmen wir an, wir haben ein einfaches, 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 angeben, wenn Mutationen (Änderungen) am Objekt vorgenommen werden:

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

Wir können diese Änderungen dann mit O.o() beobachten und das Objekt als erstes Argument und den Callback 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! Schluss mit dem Dirty Check! Die Inschrift auf deinem Grabstein sollte in Comic Sans geschrieben sein. Sehen wir uns eine andere Property an. Diesmal completeBy:

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

Wie wir sehen können, erhalten wir wieder erfolgreich einen Änderungsbericht:

Bericht ändern

Sehr gut. Was wäre, wenn wir uns nun dazu entschließen würden, die Eigenschaft „completed“ aus unserem Objekt zu 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. Wann sie gelöscht wurden. Im Grunde ändert sich der Satz der Eigenschaften eines Objekts („neu“, „gelöscht“, „neu konfiguriert“) und sein Prototyp (proto).

Wie bei jedem Beobachtungssystem gibt es auch eine Methode, um auf Veränderungen nicht zu achten. In diesem Fall ist das Object.unobserve(), das dieselbe Signatur wie O.o() hat, aber wie folgt aufgerufen werden kann:

Object.unobserve(todoModel, observer);

Wie Sie sehen, führen Mutationen, die nach der Ausführung am Objekt vorgenommen werden, nicht mehr dazu, dass eine Liste von Änderungseinträgen zurückgegeben wird.

Mutationen

Änderungen angeben, die Sie beobachten möchten

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. Nun, Beobachter können über eine Annahmeliste nur die Arten von Änderungen angeben, von denen sie erfahren möchten. Dies kann mit dem dritten Argument für O.o() wie folgt 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 löschen, wird diese Art von Änderung gemeldet:

delete todoModel.label;

Wenn Sie keine Liste mit Annahmetypen für „O.o()“ angeben, werden standardmäßig die „intrinsischen“ Objektänderungstypen verwendet (add, update, delete, reconfigure, preventExtensions (wenn ein nicht erweiterbares Objekt nicht beobachtbar ist)).

Benachrichtigungen

O.o() bietet auch Benachrichtigungen. Sie sind nichts mit lästigen Dingen vergleichbar, die man von einem Smartphone erhält. Sie sind vielmehr nützlich. Benachrichtigungen ähneln Mutationsbeobachtern. Sie werden am Ende der Mikroaufgabe angezeigt. Im Browser-Kontext befindet sich dies fast immer am Ende des aktuellen Event-Handlers.

Das Timing ist gut, da in der Regel eine Arbeitseinheit abgeschlossen ist und nun die Beobachter 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. Behalte die Kommentare hier im Blick:

// 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“). Alles andere, was bei der Implementierung des Objekts gemeldet wird (notifier.notifyChange()).

Unsere jahrelange Erfahrung auf der Webplattform hat uns gelehrt, dass man als Erstes einen synchronen Ansatz ausprobiert, 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 widerlegt 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. Am Ende müssen wir viel mehr Fehler überprü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 den zugrunde liegenden Datenobjekten gesendet. Aus Dateneigenschaften

Synthetische Änderungseinträge

Die Beobachtung von Zugriffsfunktionen und berechneten Eigenschaften kann mit notifier.notify – einem weiteren Teil von O.o() – gelöst werden. Die meisten Beobachtungssysteme wünschen sich eine Form der Beobachtung abgeleiteter Werte. Dafür gibt es viele Möglichkeiten. O.o macht keine Aussage 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.

Richten wir das nächste Beispiel auf, 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 dies getan haben, passt sich dies in Ihr System als Ganzes an.

Überspringen Sie den Code, um zu sehen, wie dies 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 Attribute oder Zugriffsfunktionen. Der Grund dafür ist, dass es bei JavaScript nicht wirklich das Konzept von Wertänderungen für die Zugriffsfunktionen 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 konnte nur ein Teil des Codes ausgeführt werden.

Das Problem ist semantisch, da wir uns die obige Zuweisung zum Wert - 5 ansehen können. Wir sollten wissen, was hier passiert ist. Das ist eigentlich ein unlösbares Problem. Das Beispiel veranschaulicht dies. Es gibt keine Möglichkeit, dass ein System weiß, was damit gemeint ist, da es sich um beliebigen Code handeln kann. Es kann in diesem Fall tun, was es will. Der Wert wird bei jedem Zugriff aktualisiert. Daher ist es nicht sinnvoll zu fragen, ob er sich geändert hat.

Mehrere Objekte mit einem einzigen Callback 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

Umfangreiche Änderungen

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

O.o() hilft dabei in Form von zwei speziellen Dienstprogrammen: notifier.performChange() und notifier.notify(), die wir bereits vorgestellt haben.

Große Änderungen

Sehen wir uns dies anhand eines Beispiels an, das zeigt, wie umfangreiche Änderungen beschrieben werden können, indem wir ein Thingy-Objekt mit einigen mathematischen Dienstprogrammen definieren (multiply, increment, incrementAndMultiply). 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
    });
  }
}

Anschließend definieren wir zwei Beobachter für unser -Objekt: einen, der als Catchall für Änderungen dient, und einen, der nur Daten zu bestimmten, von uns definierten akzeptierten Typen zurückliefert (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);
}

Wir können jetzt mit diesem Code herumspielen. Lassen Sie uns ein neues Objekt definieren:

var thingy = new Thingy(2, 4);

Sehen Sie sich die Änderungen an und nehmen Sie gegebenenfalls Änderungen vor. Wahnsinn, macht Spaß. 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 }
Umfangreiche Änderungen

Alles innerhalb der „Perform-Funktion“ wird als Arbeit einer „großen Veränderung“ betrachtet. Beobachter, die „große Veränderung“ akzeptieren, erhalten nur den Datensatz „große Veränderung“. Beobachter, die nicht die zugrunde liegenden Änderungen erhalten, die sich aus der Arbeit ergeben, die „Funktion ausführen“ ausgeführt hat.

Arrays beobachten

Wir haben schon eine Weile darüber gesprochen, Änderungen an Objekten zu beobachten, aber was ist mit Arrays?! Gute Frage. Wenn mir jemand 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 sehen Sie ein Beispiel, bei dem wir ein „Array“-Modell beobachten und eine Liste der Änderungen erhalten, wenn es Änderungen an den zugrunde liegenden Daten gibt:

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 dominiert die Häufigkeit der Schreibvorgänge.
  2. Sie können einen Cache erstellen, der den konstanten Arbeitsaufwand beim Schreiben gegen eine algorithmische Verbesserung der Leistung während der Lesevorgänge eintauscht.
  3. Die ständige Verlangsamung von Schreibvorgängen ist akzeptabel.

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

Für die schmutzige Prüfung 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? Nun, die „Dirty Check“-Methode muss jedes Mal ausgeführt werden, wenn sich Daten möglicherweise geändert haben. Es gibt einfach keine sehr stabile Methode, um dies zu erreichen, und jede Herangehensweise hat erhebliche Nachteile. So besteht z. B. das Risiko, dass bei der Prüfung eines Abfrageintervalls visuelle Artefakte auftreten und es zu Race-Bedingungen zwischen Codeproblemen kommt. Für die schmutzige Prüfung ist außerdem eine globale Registrierung von Beobachtern erforderlich, wodurch eine Gefahr von Speicherlecks entsteht und Kosten für das Entfernen durch O.o() vermieden werden.

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 mit einigen nützlichen Funktionen verwendet. 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 sagen, dass ich „foo.bar.baz“ von einem bestimmten Objekt beobachten möchte, und Sie werden informiert, wenn sich der Wert in diesem Pfad ändert. Wenn der Pfad nicht erreichbar ist, wird der Wert als nicht definiert betrachtet.

Beispiel für die Beobachtung eines Werts in 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 erhalten Informationen zu Array-Slices. 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. Dies ist der Mindestbetrag, der für den Wechsel vom alten in den neuen Status erforderlich ist.

Beispiel für die Berichterstellung mit Änderungen an einem Array als minimaler Satz von Splices:

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 große Chance, die Leistung ihrer Datenbindung in Browsern zu verbessern, die diese Funktion unterstützen.

Yehuda Katz und Erik Bryn von Ember bestätigt, dass die Unterstützung für O.o() in der kurzfristigen Roadmap von Ember ist. Misko Hervy von Angular hat eine Designdokumentation zur verbesserten Änderungserkennung in Angular 2.0 verfasst. Langfristig verfolgen sie den Ansatz, die Vorteile von Object.observe() zu nutzen, wenn sie in Chrome stabil verfügbar ist, und Watchtower.js verwenden, um bis dahin einen eigenen Ansatz zur Änderungserkennung zu nutzen. Das ist super spannend.

Ergebnisse

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

Wir hoffen, dass die Funktion in Zukunft in mehr Browsern verfügbar sein wird, sodass JavaScript-Frameworks durch den Zugriff auf native Funktionen zur Objektbeobachtung die Leistung steigern können. Diejenigen, die auf Chrome abzielen, sollten O.o() in Chrome 36 (und höher) verwenden können und 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.