Datenbindungs-Umdrehungen mit Object.observe()

Addy Osmani
Addy Osmani

Einleitung

Eine Revolution naht. Es gibt eine neue Ergänzung für JavaScript, die alles verändern wird, was Sie bereits über Datenbindung wissen. Außerdem ändert sich dadurch, wie viele Ihrer MVC-Bibliotheken Modelle für Bearbeitungen und Aktualisierungen beobachten. Bist du bereit, die Leistung von Apps zu steigern, bei denen du Immobilien beobachten möchtest?

Ok. Ok. Ich freue mich, Ihnen mitteilen zu können, dass Object.observe() jetzt in Chrome 36 (stabile Version) verfügbar ist. [ DIE MENSCHENZAHL GESCHAFFT].

Object.observe(), Teil eines zukünftigen ECMAScript-Standards, ist 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 Folge von Änderungseinträgen zu erhalten, die die Änderungen beschreiben, die an einer Reihe 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);
    });

});

Jedes Mal, wenn eine Änderung vorgenommen wird, wird diese gemeldet:

Änderung gemeldet.

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

Das heißt aber nicht, dass Sie keinen verwenden sollten. Für große Projekte mit komplizierter Geschäftslogik sind gut durchdachte Frameworks von unschätzbarem Wert und Sie sollten sie weiterhin verwenden. Sie vereinfachen die Orientierung für neue Entwickelnde, erfordern weniger Code-Wartung und legen Muster für die Bewältigung allgemeiner Aufgaben auf. Wenn Sie keine benötigen, können Sie kleinere, spezifischere Bibliotheken wie Polymer verwenden, die bereits O.o() nutzen.

Selbst wenn du häufig ein Framework oder eine MV*-Bibliothek verwendest, kann O.o() ihnen einige gesündere Leistungsverbesserungen bieten, mit einer schnelleren, einfacheren Implementierung unter Beibehaltung derselben API. Im letzten Jahr stellte Angular fest, dass in einer Benchmark, in der Änderungen an einem Modell vorgenommen wurden, 40 ms pro Update für Dirty-Checking und O.o() pro Update 1–2 ms benötigt wurden (eine Verbesserung um das 20- bis 40-Fache).

Durch die Datenbindung, ohne dass viel komplizierter Code erforderlich ist, müssen Sie außerdem nicht mehr nach Änderungen abfragen, was die Akkulaufzeit verlängert.

Wenn Sie bereits mit O.o() vertraut sind, springen Sie zur Funktionseinführung oder lesen Sie weiter, um mehr über die Probleme zu erfahren, die damit gelöst werden.

Was möchten wir beobachten?

Wenn wir von Datenbeobachtung sprechen, meinen wir in der Regel die Überwachung bestimmter Arten von Änderungen:

  • Änderungen an unbearbeiteten JavaScript-Objekten
  • Wenn Unterkünfte hinzugefügt, geändert oder gelöscht werden
  • Wenn Elemente in Arrays ein- und auseinandergeschnitten sind
  • Änderungen am Prototyp des Objekts

Die Bedeutung der Datenbindung

Die Datenbindung spielt eine wichtige Rolle, wenn es um die Trennung von Modell und Datenansichtssteuerung geht. HTML ist ein großartiger deklarativer Mechanismus, aber er ist komplett statisch. Im Idealfall geht es darum, die Beziehung zwischen Ihren Daten und dem DOM zu deklarieren und das DOM auf dem neuesten Stand zu halten. Dadurch sparen Sie Zeit und sparen Zeit, weil Sie viel sich wiederholenden Code schreiben müssen, der nur Daten zwischen dem internen Status Ihrer Anwendung oder dem Server zum und vom DOM sendet.

Datenbindung ist besonders nützlich, wenn Sie eine komplexe Benutzeroberfläche haben, in der Sie Beziehungen zwischen mehreren Eigenschaften in Ihren Datenmodellen mit mehreren Elementen in Ihren Ansichten verknüpfen müssen. Das ist bei den Single-Page-Anwendungen, die wir heute entwickeln, ziemlich üblich.

Durch die native Beobachtung von Daten im Browser bieten wir JavaScript-Frameworks (und kleinen Dienstprogrammbibliotheken, die Sie schreiben) eine Möglichkeit, Änderungen an Modelldaten zu beobachten, ohne sich auf einige der Slow Hacks zu verlassen, die derzeit weltweit genutzt werden.

So sieht die Welt heute aus

Schmutzige Prüfung

Wo haben Sie schon einmal Datenbindung gesehen? Wenn du zum Erstellen deiner Web-Apps eine moderne MV*-Bibliothek verwendest (z. B. Angular, Knockout), bist du wahrscheinlich daran gewöhnt, Modelldaten an das DOM zu binden. Zur Auffrischung ist hier ein Beispiel für eine Telefonlisten-App, in 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 den JavaScript-Code 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 findet eine sogenannte schmutzige Überprüfung statt.

Schmutzige Prüfung

Die Grundidee bei der schmutzigen Überprüfung besteht darin, dass die Bibliothek bei jeder Änderung, die sich hätten ändern könnte, überprüfen muss, ob sie sich über einen Digest- oder Änderungszyklus geändert haben. Im Fall von Angular enthält ein Digest-Zyklus alle Ausdrücke, die für die Überwachung registriert wurden, um festzustellen, ob es eine Änderung gibt. Die vorherigen Werte des Modells sind bekannt. Sollten sie sich geändert haben, wird ein Änderungsereignis ausgelöst. Für einen Entwickler besteht der Hauptvorteil hier, dass er unbearbeitete JavaScript-Objektdaten verwenden kann, die einfach zu verwenden sind und sich recht gut zusammensetzen. Der Nachteil ist, dass es ein schlechtes algorithmisches Verhalten aufweist und möglicherweise sehr teuer ist.

Schmutzige Prüfung.

Die Kosten für diesen Vorgang sind proportional zur Gesamtzahl der beobachteten Objekte. Möglicherweise muss ich das prüfen. Möglicherweise ist auch eine Möglichkeit erforderlich, eine schmutzige Prüfung auszulösen, wenn sich Daten möglicherweise geändert haben. Dafür gibt es viele clevere Tricks. Es ist unklar, ob das Ganze jemals perfekt wird.

Das Websystem sollte mehr Möglichkeiten haben, innovativ zu sein und eigene deklarative Mechanismen zu entwickeln, z.B.

  • Einschränkungsbasierte Modellsysteme
  • Automatische Persistenzsysteme (z. B. dauerhafte Änderungen an IndexedDB oder localStorage)
  • Containerobjekte (Ember, Backbone)

In Container-Objekten erstellt ein Framework Objekte, die im Inneren die Daten enthalten. Diese Personen haben Zugriff auf die Daten und können erfassen, was Sie festlegen oder erhalten, und intern übertragen. Das funktioniert gut. Sie ist relativ leistungsfähig und weist ein gutes algorithmisches Verhalten auf. Ein Beispiel für Containerobjekte mit Ember 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

Der Aufwand, herauszufinden, was sich hier geändert hat, ist proportional zur Anzahl der Dinge, die sich geändert haben. Ein weiteres Problem besteht darin, dass Sie jetzt diese andere Art von Objekt verwenden. Im Allgemeinen müssen Sie Daten, die Sie vom Server erhalten, zu diesen Objekten konvertieren, damit sie beobachtet werden können.

Dieser Code lässt sich nicht besonders gut mit vorhandenem JS-Code erstellen, da er in den meisten Fällen davon ausgeht, dass er mit Rohdaten arbeiten kann. Aber nicht für diese speziellen Objekte.

Introducing Object.observe()

Idealerweise möchten wir das Beste aus beiden Welten – eine Methode zur Beobachtung von Daten mit Unterstützung für Rohdatenobjekte (reguläre JavaScript-Objekte), wenn wir dies durch UND tun möchten, ohne ständig alles schmutzige überprüfen zu müssen. Etwas mit einem guten algorithmischen Verhalten. Etwas, das sich gut zusammensetzt und in die Plattform verankert ist. Das ist das Schöne an der Object.observe().

Damit können wir ein Objekt beobachten, Eigenschaften ändern und einen Änderungsbericht zu den Änderungen sehen. Aber genug zur Theorie – werfen wir einen Blick auf den Code.

Object.observe()

Object.observe() und Object.unobserve()

Nehmen wir an, 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 immer dann 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
  });
}

Diese Änderungen können wir dann mithilfe von O.o() beobachten, wobei das Objekt als erstes Argument und der Callback als zweites Argument übergeben werden:

Object.observe(todoModel, observer);

Beginnen wir mit den Änderungen am Todos-Modellobjekt:

todoModel.label = 'Buy some more milk';

Beim Blick auf die Konsole erhalten wir einige nützliche Informationen. Wir wissen, welche Eigenschaft sich geändert hat, wie sie geändert wurde und wie der neue Wert lautet.

Console-Bericht

Hurra! Mach Schluss mit der Schmutzprüfung! Dein Grabstein sollte in Comic Sans gemeißelt sein. Lassen Sie uns eine weitere Property ändern. Diesmal completeBy:

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

Wie Sie sehen, erhalten wir wieder erfolgreich einen Änderungsbericht:

Änderungsbericht

Sehr gut. Was passiert, wenn wir nun die Eigenschaft „abgeschlossen“ aus unserem Objekt löschen würden:

delete todoModel.completed;
Abgeschlossen

Wie Sie sehen, enthält der Bericht zu den zurückgegebenen Änderungen Informationen zur Löschung. Wie erwartet, ist der neue Wert der Eigenschaft jetzt nicht definiert. Sie wissen jetzt, 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 in jedem Beobachtungssystem gibt es auch eine Methode, um auf Veränderungen zu warten. In diesem Fall ist das Object.unobserve(), das dieselbe Signatur wie O.o() hat, aber so aufgerufen werden kann:

Object.unobserve(todoModel, observer);

Wie Sie unten sehen können, führen alle Mutationen, die an dem -Objekt vorgenommen werden, nachdem dieses ausgeführt wurde, nicht mehr dazu, dass eine Liste von Änderungseinträgen zurückgegeben wird.

Mutationen

Interessenänderungen angeben

Sie wissen jetzt, wie Sie eine Liste der Änderungen an einem beobachteten Objekt abrufen. Wie gehen Sie vor, wenn Sie nicht an allen Änderungen an einem Objekt interessiert sind, sondern nur an einem Teil? Jeder benötigt einen Spamfilter. Nun, Beobachter können über eine Annahmeliste nur die Arten von Änderungen angeben, über die sie informiert werden 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, wie dies verwendet werden kann:

// 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 jedoch jetzt das Label löschen, werden Sie feststellen, dass diese Art von Änderung gemeldet wird:

delete todoModel.label;

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

Benachrichtigungen

O.o() hat auch das Konzept der Benachrichtigungen. Sie sind nichts wie die lästigen Dinge, die man auf einem Smartphone hat, sondern vielmehr praktisch. Benachrichtigungen ähneln Mutation Observers. Sie finden am Ende der Mikroaufgabe statt. Im Browser-Kontext befindet sich dies fast immer am Ende des aktuellen Event-Handlers.

Das Timing ist gut, weil 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 Notifiers sieht in etwa so aus:

Benachrichtigungen

Sehen wir uns ein Beispiel an, wie Notifier in der Praxis verwendet werden können, um benutzerdefinierte Benachrichtigungen zu definieren, wenn Eigenschaften für ein Objekt abgerufen oder festgelegt werden. Hier kannst du die Kommentare im Auge behalten:

// 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 von der Implementierung des Objekts gemeldet wird (notifier.notifyChange()).

Aus jahrelangen Erfahrungen mit der Webplattform haben wir herausgefunden, dass ein synchroner Ansatz das Erste ist, was Sie ausprobieren, weil es am einfachsten ist, sich einen Überblick zu verschaffen. Das Problem ist, dass dadurch ein grundlegend gefährliches Verarbeitungsmodell erstellt wird. Wenn Sie Code schreiben und beispielsweise die Eigenschaft eines Objekts aktualisieren, möchten Sie nicht, dass die Aktualisierung der Eigenschaft dieses Objekts dazu führen kann, dass willkürlicher Code ausgeführt wird, was er tun soll. Es ist nicht ideal, wenn Ihre Annahmen entwertet werden, während Sie mitten in einer Funktion arbeiten.

Als Beobachterin möchten Sie idealerweise nicht angerufen werden, wenn jemand mitten in einer Sache steht. Sie möchten nicht aufgefordert werden, in einem inkonsistenten Zustand der Welt zu arbeiten. Die Fehlerprüfung ist viel umfassender. Ich versuche, deutlich mehr schlimme Situationen zu tolerieren, und im Allgemeinen ist es schwierig, mit diesem Modell zu arbeiten. Async ist schwieriger zu handhaben, aber am Ende des Tages ist es ein besseres Modell.

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

Synthetische Änderungseinträge

Wenn Sie Zugriffsfunktionen oder berechnete Attribute haben möchten, müssen Sie im Prinzip benachrichtigen, wenn sich diese Werte ändern. Es ist ein wenig mehr Aufwand, aber es ist eine Art erstklassige Funktion dieses Mechanismus. Diese Benachrichtigungen werden zusammen mit den anderen Benachrichtigungen der zugrunde liegenden Datenobjekte zugestellt. Aus Dateneigenschaften.

Synthetische Änderungseinträge

Das Beobachten von Zugriffsmethoden und berechnete Eigenschaften kann mit notifier.notify gelöst werden, einem weiteren Teil von O.o(). Die meisten Beobachtungssysteme benötigen eine Form der Beobachtung abgeleiteter Werte. Dafür gibt es viele Möglichkeiten. O.o trifft kein Urteil über den "richtigen" Weg. Berechnete Properties sollten Zugriffsfunktionen sein, die notify, wenn sich der interne (private) Status ändert.

Auch hier sollten Webdevs Bibliotheken erwarten, die die Benachrichtigung und verschiedene Ansätze für berechnete Eigenschaften erleichtern (und die Boilerplate reduzieren).

Richten wir das nächste Beispiel ein, die Kreisklasse. Die Idee ist hier, dass wir diesen Kreis haben und dass es eine Radius-Eigenschaft gibt. In diesem Fall ist der Radius eine Zugriffsfunktion. Wenn sich sein Wert ändert, benachrichtigt er sich selbst über die Änderung des Werts. Sie wird zusammen mit allen anderen Änderungen an diesem oder einem anderen Objekt übergeben. Wenn Sie ein Objekt implementieren, möchten Sie synthetische oder berechnete Attribute haben oder eine Strategie für die Funktionsweise auswählen. Danach passt er in Ihr System als Ganzes.

Überspringe den Code, um zu sehen, dass 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

Eigenschaften der Zugriffsfunktion

Ein kurzer Hinweis zu den Eigenschaften der Zugriffsfunktion. Wie bereits erwähnt, lassen sich nur die Wertänderungen für Dateneigenschaften beobachten. Nicht für berechnete Attribute oder Zugriffsfunktionen. Der Grund dafür ist, dass JavaScript nicht wirklich Wertänderungen für die Zugriffsfunktionen hat. Eine Zugriffsfunktion ist nur eine Sammlung von Funktionen.

Wird die Funktion einer Zugriffsfunktion zugewiesen, wird die Funktion dort lediglich dort aufgerufen. Von der Perspektive aus hat sich nichts geändert. Damit konnte einfach einiger Code ausgeführt werden.

Das Problem besteht darin, dass wir die obige Zuweisung des Werts - 5 des Werts semantisch - anschauen. Wir dürften in der Lage sein, zu erfahren, was hier passiert ist. Dieses Problem ist in Wirklichkeit ein unlösbares Problem. Das Beispiel zeigt die Gründe. Kein System weiß, was damit gemeint ist, da es sich um willkürlichen Code handeln kann. In diesem Fall kann er tun, was er möchte. Der Wert wird bei jedem Zugriff aktualisiert. Fragen, ob eine Änderung stattgefunden hat, macht also nicht viel Sinn.

Mehrere Objekte mit einem Callback beobachten

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

Mehrere Objekte mit einem Callback beobachten

Umfangreiche Änderungen

Vielleicht arbeiten Sie an einer wirklich großen App und müssen regelmäßig mit größeren Änderungen arbeiten. Bei Objekten können größere semantische Änderungen beschrieben werden, die sich auf viele Eigenschaften kompakter auswirken, anstatt unzählige Eigenschaftenänderungen zu senden.

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

Umfangreiche Änderungen

Betrachten wir dies anhand eines Beispiels dafür, wie umfangreiche Änderungen beschrieben werden können, wenn wir ein Thingy-Objekt mit einigen mathematischen Dienstprogrammen definieren (multiply, increment, incrementAndMultiply). Bei jeder Verwendung eines Dienstprogramms wird dem System mitgeteilt, dass eine Sammlung von Arbeiten 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
    });
  }
}

Dann definieren wir zwei Beobachter für unser Objekt: einen als Catchall für Änderungen und einen weiteren, der nur über die von uns definierten Typen von Akzeptanzdaten berichtet (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 spielen. Lassen Sie uns ein neues Ding definieren:

var thingy = new Thingy(2, 4);

Beobachten Sie ihn und nehmen Sie dann einige Änderungen vor. Oh mein Gott, 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 }
Umfangreiche Änderungen

Alles innerhalb der „Perform-Funktion“ wird als „big-change“-Arbeit betrachtet. Beobachter, die „big-change“ akzeptieren, erhalten nur den Datensatz „big-change“. Beobachter, die die zugrunde liegenden Änderungen nicht erhalten, die sich aus der Arbeit ergeben, die „Funktion ausführen“ hat.

Arrays beobachten

Wir haben schon länger über das Beobachten von Änderungen an Objekten gesprochen, aber was ist mit Arrays?! Sehr gute Frage. Wenn mir jemand sagt: „Gute Frage.“ Ich höre nie ihre Antwort, weil ich mir selbst dafür gratuliere, dass ich so eine tolle Frage stelle, aber ich schweife ab. Es gibt auch neue Methoden für die Arbeit mit Arrays!

Array.observe() ist eine Methode, die umfangreiche Änderungen an sich selbst – z. B. das Kleben, das Aufheben der Umschalttaste oder etwas, das seine Länge implizit ändert – als „splice“-Änderungseintrag behandelt. Intern wird notifier.performChange("splice",...) verwendet.

In diesem Beispiel beobachten wir ein „Array“ eines Modells und erhalten eine Liste der Änderungen, wenn Änderungen an den zugrunde liegenden Daten vorgenommen wurden:

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

Die Auswirkungen von O.o() auf die Rechenleistung können Sie sich wie einen Lese-Cache vorstellen. Im Allgemeinen ist ein Cache in folgenden Fällen eine gute Wahl (in der Reihenfolge der Wichtigkeit):

  1. Die Häufigkeit von Lesevorgängen dominiert die Häufigkeit der Schreibvorgänge.
  2. Sie können einen Cache erstellen, der die konstante Arbeit, die während der Schreibvorgänge benötigt wird, gegen eine algorithmisch bessere Leistung bei Lesevorgängen eintauscht.
  3. Die konstante Zeitverlangsamung von Schreibvorgängen ist akzeptabel.

O.o() ist auf Anwendungsfälle wie 1) ausgelegt.

Bei schmutzigen Prüfungen muss eine Kopie aller Daten gespeichert werden, die Sie beobachten. Dies bedeutet, dass Kosten für strukturelle Speicherkosten für eine schmutzige Prüfung anfallen, die mit O.o() einfach nicht möglich ist. Dirty-Checking ist zwar eine angemessene Behelfslösung, ist aber auch eine im Grunde undurchsichtige Abstraktion, die unnötige Komplexität für Anwendungen verursachen kann.

Warum? Nun, die schmutzige Prüfung muss immer dann durchgeführt werden, wenn sich Daten möglicherweise geändert haben. Es gibt einfach keine sehr robuste Methode, dies zu tun, und jede Herangehensweise hat erhebliche Nachteile (z. B. die Überprüfung eines Abfrageintervalls besteht in der Gefahr von visuellen Artefakten und Wettlaufbedingungen zwischen Codebedenken). Die schmutzige Prüfung erfordert außerdem eine globale Registrierung von Beobachtern, wodurch Gefahren durch Speicherlecks entstehen und die mit O.o() vermiedenen Kosten für das Herunterfahren entstehen.

Sehen wir uns ein paar Zahlen an.

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

Schmutzige Prüfung

Schmutzige Leistungsprüfung

Chrome mit aktiviertem Object.observe()

Leistung beobachten

Polyfilling-Objekt.observe()

Sehr gut. O.o() kann nun in Chrome 36 verwendet werden. Aber was ist mit der Verwendung in anderen Browsern? Die beantworten wir Ihnen gern. Observe-JS von Polymer ist ein Polyfill für O.o(), bei dem die native Implementierung verwendet wird, sofern vorhanden. Ansonsten wird er jedoch mit Polyfill versehen und ergänzt. Sie bietet einen Überblick über die Welt, der Änderungen fasst und einen Bericht über die Änderungen liefert. Dabei sind zwei besonders überzeugende Aspekte zu erkennen:

  1. Sie können Pfade beobachten. Sie können also sagen, dass „foo.bar.baz“ von einem bestimmten Objekt aus beobachtet werden soll und Sie wissen, wann sich der Wert auf diesem Pfad geändert hat. 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 erfahren mehr über Array-Slices. Array-Slices sind im Grunde die Mindestsplice-Vorgänge, die Sie für ein Array ausführen müssen, um die alte Version des Arrays in die neue Version des Arrays zu transformieren. Dies ist eine Art von Transformation oder eine andere Ansicht des Arrays. Dies ist der Mindestaufwand, den Sie ausführen müssen, um vom alten in den neuen Zustand zu wechseln.

Beispiel für die Meldung von Ä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 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 geplant ist. Misko Hervy von Angular hat ein Designdokument zur verbesserten Änderungserkennung von Angular 2.0 verfasst. Langfristig soll Object.observe() verwendet werden, wenn es in Chrome (stabile Version) eingebunden wird, und dann Watchtower.js verwenden, den eigenen Ansatz zur Änderungserkennung. Suuuuper aufregend.

Ergebnisse

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

Wir hoffen, dass die Funktion mit der Zeit in mehr Browsern eingeführt wird und JavaScript-Frameworks Leistungssteigerungen durch Zugriff auf Funktionen zur Beobachtung nativer Objekte ermöglicht. Wenn Sie auf Chrome ausgerichtet sind, sollte O.o() in Chrome 36 (und höher) verwendet werden können. Die Funktion sollte auch in einer zukünftigen Opera-Version verfügbar sein.

Sprechen Sie mit den Autoren von JavaScript-Frameworks über Object.observe() und darüber, wie sie damit die Leistung der Datenbindung in Ihren Apps verbessern möchten. Es stehen aufregende Zeiten vor uns!

Ressourcen

Wir bedanken uns bei Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan und Vivian Cromwell für ihren Beitrag und ihre Rezensionen.