Rivoluzioni dell'associazione dati con Object.observe()

Addy Osmani
Addy Osmani

Introduzione

È in arrivo una rivoluzione. C'è una nuova funzionalità in JavaScript che cambierà tutto ciò che pensi di sapere sul binding dei dati. Inoltre, cambierà il numero di librerie MVC che si occupano di osservare i modelli per le modifiche e gli aggiornamenti. Sei pronto per un aumento del rendimento delle app che si occupano dell'osservazione delle proprietà?

Ok. Senza ulteriori ritardi, sono felice di annunciare che Object.observe() è disponibile nella versione stabile di Chrome 36. [WOOOO. LA FOLLA VA SELVAGGIA].

Object.observe(), parte di uno standard ECMAScript futuro, è un metodo per osservare in modo asincrono le modifiche agli oggetti JavaScript... senza la necessità di una libreria separata. Consente a un osservatore di ricevere una sequenza temporale di record di modifiche che descrivono l'insieme di cambiamenti avvenuti in un insieme di oggetti osservati.

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

});

Ogni volta che viene apportata una modifica, viene segnalata:

Modifica segnalata.

Con Object.observe() (mi piace chiamarla O.o() o Oooooooo), puoi implementare l'associazione dei dati bidirezionale senza bisogno di un framework.

Questo non vuol dire che sia giusto usarne uno. Per progetti di grandi dimensioni con logica aziendale complessa, i framework con opinioni sono preziosi e dovresti continuare a utilizzarli. Semplificano l'orientamento dei nuovi sviluppatori, richiedono una minore manutenzione del codice e impongono modelli su come svolgere le attività comuni. Se non ne hai bisogno, puoi utilizzare librerie più piccole e mirate come Polymer (che già sfruttano O.o()).

Anche se utilizzi molto un framework o una libreria MV*, O.o() ha il potenziale per fornire alcuni miglioramenti significativi delle prestazioni, con un'implementazione più rapida e semplice, mantenendo la stessa API. Ad esempio, l'anno scorso Angular ha scoperto che in un benchmark in cui venivano apportate modifiche a un modello, il controllo dell'integrità richiedeva 40 ms per aggiornamento e O.o() 1-2 ms per aggiornamento (un miglioramento di 20-40 volte più veloce).

Il binding dei dati senza la necessità di utilizzare tonnellate di codice complicato significa anche che non devi più eseguire il polling per rilevare le modifiche, quindi la durata della batteria è maggiore.

Se hai già deciso di utilizzare O.o(), vai all'introduzione della funzionalità o continua a leggere per saperne di più sui problemi che risolve.

Cosa vogliamo osservare?

Quando parliamo di osservazione dei dati, di solito ci riferiamo al tenere d'occhio alcuni tipi specifici di cambiamenti:

  • Modifiche agli oggetti JavaScript non elaborati
  • Quando vengono aggiunte, modificate ed eliminate le proprietà
  • Quando gli array hanno elementi uniti e fuori
  • Modifiche al prototipo dell'oggetto

L'importanza dell'associazione dei dati

L'associazione dei dati inizia a diventare importante quando ti interessa la separazione dei controlli di visualizzazione modello e visualizzazione. L'HTML è un ottimo meccanismo dichiarativo, ma è completamente statico. Idealmente, vuoi semplicemente dichiarare la relazione tra i tuoi dati e il DOM e mantenerlo aggiornato. In questo modo puoi risparmiare molto tempo evitando di scrivere codice ripetitivo che invia dati da e verso il DOM tra lo stato interno dell'applicazione o il server.

L'associazione di dati è particolarmente utile quando hai un'interfaccia utente complessa in cui devi collegare le relazioni tra più proprietà nei tuoi modelli di dati con più elementi nelle tue visualizzazioni. Si tratta di una situazione piuttosto comune nelle applicazioni a pagina singola che stiamo creando oggi.

Creando un modo per osservare in modo nativo i dati nel browser, offriamo ai framework JavaScript (e alle piccole librerie di utilità che scrivi) un modo per osservare le modifiche ai dati del modello senza fare affidamento su alcuni dei lenti hack usati oggi.

L'aspetto attuale del mondo

Verifica inappropriata

Dove hai mai visto l'associazione dei dati in passato? Se utilizzi una libreria MV* moderna per creare le tue web app (ad es. Angular, Knockout), probabilmente hai dimestichezza con il binding dei dati del modello al DOM. Ecco un esempio di app Elenco di telefoni in cui viene associato il valore di ogni telefono in un array phones (definito in JavaScript) a un elemento dell'elenco in modo che i dati e l'interfaccia utente siano sempre sincronizzati:

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

e JavaScript per il 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.'}
  ];
});

Ogni volta che i dati del modello sottostante cambiano, il nostro elenco nel DOM viene aggiornato. In che modo Angular ottiene questo risultato? Beh, dietro le quinte c'è una cosa che si chiama "sporco controllo".

Controllo sporco

L'idea di base del controllo della modifica è che ogni volta che i dati potrebbero essere cambiati, la libreria deve verificare se sono stati modificati tramite un digest o un ciclo di modifica. Nel caso di Angular, un ciclo digest identifica tutte le espressioni registrate per essere controllate per vedere se c'è un cambiamento. Sa grazie ai valori precedenti di un modello e, se sono stati modificati, viene attivato un evento di modifica. Uno sviluppatore ha il vantaggio principale di usare dati di oggetti JavaScript non elaborati, che sono piacevoli da usare e hanno una buona composizione. Lo svantaggio è che ha un comportamento algoritmico scadente e che potrebbe essere molto costoso.

Controllo sporco.

Il costo di questa operazione è proporzionale al numero totale di oggetti osservati. Potrei dover fare un sacco di controlli sporchi. Potrebbe inoltre essere necessario trovare un modo per attivare il controllo "sporco" quando i dati potrebbero cambiare. A questo scopo, esistono molti altri trucchi intelligenti. Non è chiaro se questo sarà mai perfetto.

L'ecosistema web dovrebbe avere una maggiore capacità di innovare ed evolvere i propri meccanismi dichiarativi, ad esempio

  • Sistemi di modelli basati su vincoli
  • Sistemi a persistenza automatica (ad esempio modifiche persistenti a IndexedDB o localStorage)
  • Oggetti container (Ember, Backbone)

Gli oggetti container sono i punti in cui un framework crea oggetti che all'interno contengono i dati. Hanno accessori ai dati e possono acquisire ciò che imposti o ottieni e trasmettere internamente. Funziona bene. Ha prestazioni relativamente elevate e buon comportamento algoritmico. Di seguito è riportato un esempio di oggetti contenitore che utilizzano 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

La spesa per scoprire cosa è cambiato qui è proporzionale al numero di cose che sono cambiate. Un altro problema è che ora stai utilizzando questo tipo di oggetto diverso. In genere, devi convertire i dati che ricevi dal server in questi oggetti in modo che siano osservabili.

Questo non si compone particolarmente bene con il codice JS esistente perché la maggior parte del codice presuppone di poter operare su dati non elaborati. Non per questi oggetti specializzati.

Introducing Object.observe()

Idealmente, vogliamo il meglio di entrambi i mondi: un modo per osservare i dati con il supporto di oggetti di dati non elaborati (regolari oggetti JavaScript) se lo scegliamo E senza dover controllare sempre tutto. Deve avere un buon comportamento algoritmico. Qualcosa che si compone bene ed è integrato nella piattaforma. Questo è il bello di ciò che offre Object.observe().

Ci consente di osservare un oggetto, modificare le proprietà e visualizzare il report delle modifiche di ciò che è cambiato. Ma abbastanza della teoria, diamo un'occhiata a un po' di codice!

Object.observe()

Object.observe() e Object.unobserve()

Supponiamo di avere un semplice oggetto JavaScript "vanilla" che rappresenta un modello:

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

Possiamo quindi specificare un callback ogni volta che vengono apportate mutazioni (modifiche) all'oggetto:

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

Possiamo quindi osservare queste modifiche utilizzando O.o(), passando l'oggetto come primo argomento e il callback come secondo:

Object.observe(todoModel, observer);

Iniziamo ad apportare alcune modifiche all'oggetto del modello di cose da fare:

todoModel.label = 'Buy some more milk';

Dalla console otteniamo alcune informazioni utili. Sappiamo quale proprietà è stata modificata, come è stata modificata e qual è il nuovo valore.

Report della console

Evviva! Addio, sporco controllo! La tua lapide deve essere scolpita in Comic Sans. Cambiamo un'altra proprietà. Questa volta completeBy:

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

Abbiamo notato che riusciamo a recuperare un rapporto sulle modifiche anche questa volta:

Modifica il report.

Ottimo. E se ora decidessimo di eliminare la proprietà "complete" dal nostro oggetto:

delete todoModel.completed;
Completato

Come possiamo vedere, il report delle modifiche restituite include informazioni sull'eliminazione. Come previsto, il nuovo valore della proprietà ora non è definito. Ora sappiamo che puoi sapere quando sono state aggiunte le proprietà. Quando sono stati eliminati. In sostanza, l'insieme di proprietà di un oggetto ("new", "deleted", "reconfigured") e la modifica del suo prototipo (proto).

Come in ogni sistema di osservazione, esiste anche un metodo per interrompere l'ascolto dei cambiamenti. In questo caso, si tratta di Object.unobserve(), che ha la stessa firma di O.o() ma può essere chiamato come segue:

Object.unobserve(todoModel, observer);

Come possiamo vedere di seguito, eventuali mutazioni apportate all'oggetto dopo l'esecuzione non generano più un elenco di record di variazione.

Mutazioni

Specificare le variazioni di interesse

Abbiamo quindi esaminato le nozioni di base su come recuperare un elenco delle modifiche a un oggetto osservato. Cosa succede se ti interessa solo un sottoinsieme di modifiche apportate a un oggetto anziché tutte? Tutti hanno bisogno di un filtro antispam. Gli osservatori possono specificare solo i tipi di modifiche di cui vogliono essere informati tramite un elenco di accettazione. Questo può essere specificato utilizzando il terzo argomento di O.o() come segue:

Object.observe(obj, callback, optAcceptList)

Vediamo un esempio di come può essere utilizzato:

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

Tuttavia, se ora eliminiamo l'etichetta, tieni presente che questo tipo di modifica viene registrato:

delete todoModel.label;

Se non specifichi un elenco di tipi accettati per O.o(), per impostazione predefinita vengono applicati i tipi di modifica degli oggetti "intrinsici" (add, update, delete, reconfigure, preventExtensions (per quando un oggetto che diventa non estensibile non è osservabile)).

Notifiche

O.o() include anche la nozione di notifiche. Non sono come quelle fastidiose cose che trovi su un telefono, ma piuttosto utili. Le notifiche sono simili agli osservatori delle mutazioni. Vengono eseguiti al termine della micro-attività. Nel contesto del browser, quasi sempre si trova alla fine del gestore di eventi corrente.

Il tempismo è buono perché in genere un'unità di lavoro è terminata e ora gli osservatori possono svolgere il proprio lavoro. È un buon modello di elaborazione a turni.

Il flusso di lavoro per l'utilizzo di un generatore di notifiche è simile al seguente:

Notifiche

Vediamo un esempio di come i notificatori potrebbero essere utilizzati in pratica per definire notifiche personalizzate per quando le proprietà di un oggetto vengono recuperate o impostate. Tieni d'occhio i commenti qui:

// 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);
Console Notifiche

Qui registriamo quando il valore delle proprietà dei dati cambia ("update"). Qualsiasi altro elemento che l'implementazione dell'oggetto sceglie di segnalare (notifier.notifyChange()).

Anni di esperienza sulla piattaforma web ci hanno insegnato che un approccio sincrono è la prima cosa da provare perché è il più facile da comprendere. Il problema è creare un modello di elaborazione sostanzialmente pericoloso. Se stai scrivendo codice e, ad esempio, aggiorni la proprietà di un oggetto, non vuoi che si verifichi una situazione in cui l'aggiornamento della proprietà dell'oggetto possa aver invitato un codice arbitrario a fare ciò che voleva. Non è l'ideale se le ipotesi vengano invalidate mentre stai eseguendo una funzione.

Se sei un osservatore, idealmente non vorrai essere chiamato se qualcuno si trova nel bel mezzo di qualcosa. Non vuoi che ti venga chiesto di andare a lavorare su una situazione incoerente del mondo. finiranno per eseguire molti altri controlli di errore. Cerca di tollerare molte più situazioni negative e, in genere, è un modello difficile da utilizzare. L'elaborazione asincrona è più complessa, ma alla fine è un modello migliore.

La soluzione a questo problema è rappresentata dai record di modifica sintetici.

Record di modifiche sintetiche

Fondamentalmente, se vuoi avere funzioni di accesso o proprietà calcolate, è tua responsabilità avvisare quando questi valori cambiano. Richiede un piccolo lavoro aggiuntivo, ma è stato progettato come una sorta di funzionalità all'avanguardia di questo meccanismo e queste notifiche verranno inviate insieme alle altre notifiche provenienti da oggetti dati sottostanti. Dalle proprietà dei dati.

Record delle modifiche sintetiche

L'osservazione degli accessori e delle proprietà calcolate può essere risolta con notifier.notify, un'altra parte di O.o(). La maggior parte dei sistemi di osservazione richiede una qualche forma di osservazione dei valori derivati. Esistono molti modi per farlo. O.o non esprime alcun giudizio in merito al modo "corretto". Le proprietà calcolate devono essere funzioni di accesso che inviano una notifica quando lo stato interno (privato) cambia.

Anche in questo caso, i webdev dovrebbero aspettarsi che le librerie contribuiscano a semplificare le notifiche e i vari approcci alle proprietà calcolate (e a ridurre il boilerplate).

Configura l'esempio successivo, ovvero una classe cerchio. L'idea è che abbiamo questo cerchio e una proprietà raggio. In questo caso, il raggio è una funzione di accesso e, quando il valore cambia, il valore viene notificato. Verrà pubblicato insieme a tutte le altre modifiche all'oggetto o a qualsiasi altro oggetto. In sostanza, se stai implementando un oggetto in cui vuoi avere proprietà sintetiche o calcolate, devi scegliere una strategia per il funzionamento. Dopodiché, questa operazione si adatterà all'intero sistema.

Salta il codice per vedere come funziona in DevTools.

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);
  })
}
Console dei record delle modifiche sintetiche

Proprietà degli accessori

Una breve nota sulle proprietà degli accessori. Come accennato prima, solo le modifiche ai valori sono osservabili per le proprietà dei dati. Non per le proprietà o le funzioni di accesso calcolate. Il motivo è che JavaScript in realtà non ha la nozione di modifiche ai valori delle funzioni di accesso. Una funzione di accesso è semplicemente una raccolta di funzioni.

Se assegni a una funzione di accesso, JavaScript richiama semplicemente la funzione in quel punto e dal suo punto di vista non è cambiato nulla. Ha appena dato l'opportunità di eseguire un po' di codice.

Il problema è semanticamente che possiamo guardare all'assegnazione precedente al valore - 5. Dovremmo essere in grado di sapere cosa è successo qui. Si tratta di un problema irrisolvibile. L'esempio mostra il motivo. Non esiste alcun modo per qualsiasi sistema di sapere cosa si intende per questo, perché può essere un codice arbitrario. In questo caso può fare ciò che vuole. Il valore viene aggiornato ogni volta che viene eseguito l'accesso, pertanto non ha molto senso chiedersi se è cambiato.

Osservazione di più oggetti con un solo callback

Un altro pattern possibile con O.o() è la nozione di singolo osservatore di callback. In questo modo, un singolo callback può essere utilizzato come "osservatore" per molti oggetti diversi. Al callback verrà inviato l'intero insieme di modifiche a tutti gli oggetti che osserva alla "fine della microtask" (nota la somiglianza con gli osservatori delle mutazioni).

Osservazione di più oggetti con un solo callback

Modifiche su larga scala

Forse stai lavorando a un'app molto grande e devi regolarmente gestire modifiche su larga scala. Gli oggetti potrebbero voler descrivere cambiamenti semantici più ampi, il che influirà su molte proprietà in modo più compatto (anziché trasmettere moltissime modifiche delle proprietà).

O.o() aiuta a farlo attraverso due utilità specifiche: notifier.performChange() e notifier.notify(), che abbiamo già introdotto.

Modifiche su larga scala

Vediamo un esempio di come descrivere le modifiche su larga scala in cui definiamo un oggetto Thingy con alcune utilità matematiche (multiply, increment, incrementAndMultiply). Ogni volta che viene utilizzata un'utilità, viene comunicato al sistema che una raccolta di lavori comprende un tipo specifico di modifica.

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

Definiamo quindi due osservatori per il nostro oggetto: uno che è un catch-all per le modifiche e un altro che riporta solo i tipi di accettazione specifici che abbiamo definito (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);
}

Ora possiamo iniziare a giocare con questo codice. Definiamo una nuova cosa:

var thingy = new Thingy(2, 4);

Osservala e poi apporta alcune modifiche. Wow, che divertimento. Tante cose!

// 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 }
Modifiche su larga scala

Tutto ciò che si trova all'interno della "funzione di esecuzione" è considerato opera di "grande-modifica". Gli osservatori che accettano "grande-modifica" riceveranno solo il record "grande-modifica". Gli osservatori che non lo fanno riceveranno le modifiche sottostanti risultanti dal lavoro svolto dalla "funzione di esecuzione".

Osservare gli array

Abbiamo parlato a lungo di come osservare le modifiche agli oggetti, ma che dire degli array? Ottima domanda. Quando qualcuno mi dice "Ottima domanda". Non sento mai la loro risposta perché sono troppo impegnato a congratularmi con me stesso per aver fatto una domanda così eccezionale, ma sto divagando. Esistono anche nuovi metodi per lavorare con gli array.

Array.observe() è un metodo che tratta le modifiche su larga scala apportate a se stesso, ad esempio splice, unshift o qualsiasi operazione che ne modifichi implicitamente la durata, come un record di modifiche "splice". Internamente utilizza notifier.performChange("splice",...).

Ecco un esempio in cui osserviamo un "array" di modelli e, in modo simile, riceviamo un elenco di modifiche quando si verificano modifiche ai dati sottostanti:

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';
Osservare gli array

Prestazioni

L'impatto sulle prestazioni computazionali di O.o() è paragonabile a una cache di lettura. In generale, una cache è un'ottima scelta quando (in ordine di importanza):

  1. La frequenza delle letture domina quella delle scritture.
  2. Puoi creare una cache che scambia la quantità costante di lavoro richiesta durante le scritture in modo da migliorare le prestazioni algoritmiche durante le letture.
  3. Il rallentamento costante delle scritture è accettabile.

O.o() è progettato per casi d'uso come 1).

Il controllo parziale richiede la conservazione di una copia di tutti i dati che stai osservando. Ciò significa che dovrai sostenere un costo di memoria strutturale per il controllo dell'integrità che non hai con O.o(). Il controllo dell'integrità, pur essendo una soluzione temporanea decente, è anche un'astrazione fondamentalmente inaffidabile che può creare complessità non necessarie per le applicazioni.

Perché? Beh, il controllo sporco deve essere eseguito ogni volta che i dati possono essere cambiati. Non esiste un modo molto efficace per farlo e qualsiasi approccio al riguardo presenta svantaggi significativi (ad esempio, il controllo su un intervallo di polling rischia di esporre artefatti visivi e racecondition tra problemi di codice). Il controllo della modifica richiede anche un registry globale di osservatori, creando rischi di perdite di memoria e costi di smantellamento evitati da O.o().

Vediamo alcuni numeri.

I seguenti test di benchmark (disponibili su GitHub) ci consentono di confrontare il controllo dell'integrità con O.o(). Sono strutturati come grafici di dimensione dell'insieme di oggetti osservati rispetto al numero di mutazioni. Il risultato generale è che le prestazioni del controllo dirty sono proporzionali tramite un algoritmo al numero di oggetti osservati, mentre le prestazioni di O.o() sono proporzionali al numero di mutazioni che sono state effettuate.

Verifica sporca

Rendimento del controllo sporco

Chrome con Object.observe() attivato

Osserva il rendimento

Oggetto Polyfill.observe()

Ottimo, quindi O.o() può essere utilizzato in Chrome 36, ma come si può utilizzare in altri browser? Qui troverai tutte le risposte. Observe-JS di Polymer è un polyfill per O.o() che utilizzerà l'implementazione nativa se presente, altrimenti la eseguirà il polyfill e includerà alcune funzionalità utili. Offre una visione aggregata del mondo che riassume le modifiche e genera un report su cosa è cambiato. Due funzionalità molto potenti che mette a disposizione sono:

  1. Puoi osservare i percorsi. Ciò significa che si può dire che io voglia osservare "foo.bar.baz" da un determinato oggetto e che vi dirà quando il valore in quel percorso è cambiato. Se il percorso non è raggiungibile, viene considerato il valore non definito.

Esempio di osservazione di un valore in un percorso da un determinato oggetto:

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. Fornisce informazioni sulle giunzioni degli array. Gli inserimenti in un array sono fondamentalmente l'insieme minimo di operazioni di inserimento che dovrai eseguire su un array per trasformare la vecchia versione dell'array nella nuova versione. Si tratta di un tipo di trasformazione o di una vista diversa dell'array. Si tratta del lavoro minimo che devi svolgere per passare dallo stato precedente a quello nuovo.

Esempio di come vengono registrate le modifiche a un array come un insieme minimo di giunti:

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

Framework e Object.observe()

Come accennato, O.o() offrirà a framework e librerie un'enorme opportunità per migliorare le prestazioni del data binding nei browser che supportano la funzionalità.

Yehuda Katz ed Erik Bryn di Ember hanno confermato che l'aggiunta del supporto per O.o() è nella roadmap a breve termine di Ember. Misko Hervy di Angular ha scritto una documentazione di progettazione sul rilevamento delle modifiche migliorato di Angular 2.0. L'approccio a lungo termine consisterà nell'sfruttare Object.observe() quando arriverà alla versione stabile di Chrome, optando per Watchtower.js, il suo approccio di rilevamento dei cambiamenti fino ad allora. Suuuper eccitante.

Conclusioni

La funzione O.o() è una potente aggiunta alla piattaforma web che puoi utilizzare subito.

Ci auguriamo che, nel tempo, la funzionalità venga implementata in più browser, consentendo ai framework JavaScript di migliorare il rendimento grazie all'accesso alle funzionalità di osservazione degli oggetti nativi. Gli utenti che hanno come target Chrome dovrebbero essere in grado di utilizzare O.o() in Chrome 36 (e versioni successive) e la funzionalità dovrebbe essere disponibile anche in una versione futura di Opera.

Quindi, non esitare a parlare con gli autori dei framework JavaScript di Object.observe() e di come intendono utilizzarlo per migliorare il rendimento del binding dei dati nelle tue app. Ci saranno ancora periodi entusiasmanti in futuro.

Risorse

Grazie a Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan e Vivian Cromwell per il loro contributo e le loro recensioni.