Rivoluzioni dell'associazione dati con Object.observe()

Addy Osmani
Addy Osmani

Introduzione

È in arrivo una rivoluzione. Una nuova aggiunta a JavaScript cambierà tutto ciò che pensi di sapere sull'associazione dati. Cambierà anche il modo in cui molte delle tue librerie MVC si approcciano all'osservazione dei modelli per le modifiche e gli aggiornamenti. Vuoi migliorare le prestazioni delle app che interessano l'osservazione della proprietà?

OK. Ok. Senza ulteriori ritardi, sono felice di annunciare che Object.observe() è disponibile nella versione stabile di Chrome 36. [WOOOO. THE CROWD GOES WILD].

Object.observe(), che fa parte di un futuro standard ECMAScript, è 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 delle modifiche che descrivono l'insieme di modifiche avvenute 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, questa viene segnalata:

Modifica segnalata.

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

Questo non significa che non devi usarne uno. Per i grandi progetti con logica di business complicata, i framework "guidati" sono preziosi e dovresti continuare a utilizzarli. Semplificano l'orientamento dei nuovi sviluppatori, richiedono meno manutenzione del codice e impongono pattern su come svolgere le attività comuni. Quando non ti serve, puoi usare librerie più piccole e più mirate come Polymer (che sfrutta già O.o()).

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

Grazie all'associazione dei dati che non richiede un'enorme quantità di codice complicato, non dovrai più chiedere le modifiche e aumentare così la durata della batteria.

Se hai già definito O.o() per vendere, passa direttamente all'introduzione della funzione 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 a tenere d'occhio alcuni tipi specifici di cambiamenti:

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

L'importanza dell'associazione dati

L'associazione dati inizia a diventare importante quando ti interessa la separazione dei controlli di visualizzazione modello. L'HTML è un ottimo meccanismo dichiarativo, ma è completamente statico. Idealmente, vuoi solo dichiarare la relazione tra i tuoi dati e il DOM e mantenere il DOM aggiornato. Ciò consente di risparmiare tempo durante la scrittura di codice davvero ripetitivo che invia dati da e verso il DOM tra lo stato interno dell'applicazione e il server.

L'associazione dati è particolarmente utile quando hai un'interfaccia utente complessa in cui devi collegare più proprietà nei modelli dei dati con più elementi nelle viste. Questo è piuttosto comune nelle applicazioni a pagina singola che stiamo creando oggi.

Fornendo un modo per osservare i dati in modo nativo nel browser, offriamo ai framework JavaScript (e alle piccole librerie di utilità che scrivi) un modo per osservare le modifiche ai dati dei modelli senza fare affidamento su alcuni degli attacchi lenti attualmente in uso in tutto il mondo.

Il mondo attuale

Controllo in caso di sporcizia

Dove hai mai visto prima l'associazione dati? Se utilizzi una libreria MV* moderna per creare le tue app web (ad es.Angular, Knockout), probabilmente sei abituato ad associare i dati del modello al DOM. Ti ricordo di seguito un esempio di app Phone List in cui associamo il valore di ogni telefono di 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 il codice 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 sottostanti cambiano, il nostro elenco nel DOM viene aggiornato. In che modo Angular raggiunge questo obiettivo? Beh, dietro le quinte c'è qualcosa chiamato "sporco controllo".

Controllo non consentito

L'idea alla base del controllo sporco è che ogni volta che i dati potevano essere cambiati, la libreria deve controllare se sono cambiati tramite un digest o un ciclo di modifica. Nel caso di Angular, un ciclo digest identifica tutte le espressioni registrate da controllare per vedere se c'è una modifica. Conosce i valori precedenti di un modello e, se sono stati modificati, viene attivato un evento di modifica. Per uno sviluppatore, il vantaggio principale è che puoi utilizzare dati non elaborati degli oggetti JavaScript, piacevoli da usare e compositi abbastanza bene. Il lato negativo è che ha un cattivo comportamento algoritmico ed è potenzialmente molto costoso.

Controllo non sporco.

La spesa di questa operazione è proporzionale al numero totale di oggetti osservati. Potrei dover fare dei controlli sporchi. Potrebbe anche essere necessario un modo per attivare il controllo sporco quando i dati potrebbero essere cambiati. Esistono moltissimi framework di trucchi intelligenti per farlo. Non è chiaro se potrà mai essere perfetto.

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

  • Sistemi di modelli basati su vincoli
  • Sistemi a persistenza automatica (ad es.modifiche persistenti a IndexedDB o localStorage)
  • Oggetti container (bianco, backbone)

Gli oggetti container sono il punto in cui un framework crea oggetti che al suo interno contengono i dati. Hanno accesso ai dati e possono acquisire ciò che imposti o ricevi e trasmetti internamente. Questo approccio funziona bene. È relativamente performante e ha un buon comportamento algoritmico. Di seguito è riportato un esempio di oggetti container 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

Il costo di scoprire cosa è cambiato qui è proporzionale al numero di cose che sono cambiate. Un altro problema è l'utilizzo di questo tipo di oggetto diverso. In genere, devi convertire i dati ricevuti dal server in questi oggetti, in modo che siano osservabili.

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

Introducing Object.observe()

Idealmente, quello che vogliamo è il meglio di entrambi i mondi: un modo per osservare i dati con supporto per oggetti di dati non elaborati (oggetti JavaScript regolari) se scegliamo AND senza la necessità di eseguire controlli sporchi tutto il tempo. Qualcosa con un buon comportamento algoritmico. qualcosa che sia ben composto e integrato nella piattaforma. Questo è il grande vantaggio di Object.observe().

Ci permette di osservare un oggetto, modificarne le proprietà e vedere il report delle modifiche su ciò che è cambiato. Ma abbastanza riguardo alla teoria, diamo un'occhiata a un po' di codice.

Object.observe()

Object.observe() e Object.unobserve()

Immaginiamo di avere un oggetto JavaScript vanilla semplice che rappresenta un modello:

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

Possiamo quindi specificare un callback per 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 variazioni 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 Todos:

todoModel.label = 'Buy some more milk';

Se guardiamo la console, troviamo alcune informazioni utili. Sappiamo quale proprietà è cambiata, come è stata cambiata e qual è il nuovo valore.

Report della console

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

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

Come possiamo vedere, siamo nuovamente riusciti a recuperare un report sulle modifiche:

Report sulle modifiche.

Bene. E se ora decidessimo di eliminare la proprietà "completed" dal nostro oggetto:

delete todoModel.completed;
Completato

Come puoi vedere, il report sulle modifiche restituito include informazioni sull'eliminazione. Come previsto, il nuovo valore della proprietà ora non è definito. Quindi, ora sappiamo che puoi scoprire quando sono state aggiunte proprietà. Quando sono stati eliminati. Essenzialmente, l'insieme di proprietà su un oggetto ("nuovo", "eliminato", "riconfigurato") e la relativa modifica del prototipo (proto).

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

Object.unobserve(todoModel, observer);

Come puoi vedere di seguito, eventuali modifiche apportate all'oggetto dopo l'esecuzione non comportano più la restituzione di un elenco di record delle modifiche.

Mutazioni

Specificare le variazioni di interesse

Abbiamo quindi visto le nozioni di base su come recuperare un elenco delle modifiche a un oggetto osservato. E se ti interessasse 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 cambiamenti di cui vogliono essere a conoscenza 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 utilizzare questo strumento:

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

Se invece ora eliminiamo l'etichetta, noterai che questo tipo di modifica viene segnalata:

delete todoModel.label;

Se non specifichi un elenco di tipi di accettazione in O.o(), per impostazione predefinita verranno utilizzati i tipi di modifica dell'oggetto "intrinseci" (add, update, delete, reconfigure, preventExtensions (quando un oggetto che diventa non estendibile non è osservabile)).

Notifiche

O.o() viene fornito anche con la nozione di notifiche. Non sono niente come le cose fastidiose che vedi sul telefono, ma piuttosto utili. Le notifiche sono simili alla funzionalità Osservatori mutazione. ma che avvengono alla fine del micro-tasking. Nel contesto del browser, questo si trova quasi sempre alla fine del gestore di eventi corrente.

Il tempismo è adeguato perché in genere un'unità di lavoro è terminata e ora gli osservatori possono lavorare. È un bel modello di elaborazione a turni.

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

Notifiche

Vediamo un esempio di come le notifiche possono essere usate nella pratica per definire notifiche personalizzate relative al momento in cui le proprietà di un oggetto vengono ricevute 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 segnaliamo quando il valore delle proprietà dei dati cambia ("aggiornamento"). Qualsiasi altro elemento che l'implementazione dell'oggetto sceglie di segnalare (notifier.notifyChange()).

Anni di esperienza sulla piattaforma web ci hanno insegnato che l'approccio sincrono è la prima cosa da fare, perché è il più facile da capire. Il problema è creare un modello di elaborazione fondamentalmente pericoloso. Se scrivi codice e indichi di aggiornare la proprietà di un oggetto, non è consigliabile che una situazione con l'aggiornamento della proprietà dell'oggetto avrebbe potuto invitare un codice arbitrario a fare ciò che volesse. Non è ideale che le tue ipotesi vengano invalidate mentre ti trovi nel mezzo di una funzione.

Se ti occupi di osservazione, idealmente non dovresti essere chiamato se qualcuno si trova nel bel mezzo di qualcosa. Non vuoi che ti venga chiesto di lavorare su una situazione incoerente nel mondo. Finiscono per fare molti più controlli degli errori. Cercare di tollerare molte più cattive situazioni e in generale è un modello difficile con cui lavorare. Il modello asincrono è più difficile da gestire, ma in fin dei conti è migliore.

La soluzione a questo problema sono i record delle modifiche sintetiche.

Record delle modifiche sintetici

In sostanza, se vuoi avere funzioni di accesso o proprietà calcolate, è tua responsabilità avvisare quando questi valori cambiano. Si tratta di un piccolo lavoro aggiuntivo, ma è progettato come una sorta di funzionalità di prim'ordine di questo meccanismo e queste notifiche verranno inviate con il resto di quelle degli oggetti dati sottostanti. Dalle proprietà dei dati.

Record delle modifiche sintetici

Osservando le funzioni di accesso e le proprietà calcolate si può risolvere con notifier.notify, un'altra parte di O.o(). La maggior parte dei sistemi di osservazione vuole una qualche forma di osservazione dei valori derivati. Ci sono molti modi per farlo. O.o non giudica il modo "giusto". Le proprietà calcolate devono essere funzioni di accesso che inviano una notify quando lo stato interno (privato) cambia.

Ancora una volta, gli sviluppatori web devono aspettarsi che le librerie contribuiscano a semplificare le notifiche e i vari approcci alle proprietà calcolate (e a ridurre il boilerplate).

Impostiamo l'esempio successivo, una classe Circle. L'idea qui è che abbiamo questo cerchio e esiste una proprietà raggio. In questo caso il raggio è una funzione di accesso e, quando il suo valore cambia, viene avvisato automaticamente che il valore è cambiato. Verrà consegnato con tutte le altre modifiche a questo oggetto o a qualsiasi altro oggetto. In pratica, se implementi un oggetto, vuoi avere proprietà sintetiche o calcolate oppure devi scegliere una strategia per il funzionamento. Dopodiché, questa operazione si adatterà all'intero impianto.

Vai oltre il codice per vedere se 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 record di modifiche sintetici

Proprietà della funzione di accesso

Una nota rapida sulle proprietà della funzione di accesso. Come accennato in precedenza, solo le modifiche dei valori sono osservabili per le proprietà dei dati. Non per le proprietà calcolate o le funzioni di accesso. Il motivo è che JavaScript non considera le modifiche dei valori delle funzioni di accesso. Una funzione di accesso è solo una raccolta di funzioni.

Se assegni a una funzione di accesso, JavaScript richiama semplicemente la funzione in quella funzione 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 al nostro compito qui sopra al valore - 5. Dobbiamo essere in grado di sapere cosa è successo qui. In realtà, questo è un problema irrisolvibile. L'esempio ne spiega il motivo. Nessun sistema può davvero capire cosa si intende per codice perché può essere un codice arbitrario. In questo caso può fare tutto ciò che vuole. Il valore viene aggiornato ogni volta che vi si accede, quindi non ha senso chiedere se è stato modificato.

Osservazione di più oggetti con un solo callback

Un altro schema possibile con O.o() è la nozione di un singolo osservatore di callback. Ciò consente di utilizzare un singolo callback come "osservatore" per molti oggetti diversi. Il callback riceverà l'insieme completo delle modifiche a tutti gli oggetti che osserva alla "fine del microtask" (nota la somiglianza con "Osservatori mutazioni").

Osservazione di più oggetti con un solo callback

Modifiche su larga scala

Magari lavori su un'app molto grande e devi lavorare regolarmente con modifiche su larga scala. Gli oggetti potrebbero voler descrivere cambiamenti semantici maggiori che influiscono su molte proprietà in un modo più compatto (anziché trasmettere tonnellate di modifiche di proprietà).

O.o() è utile per farlo sotto forma di due utilità specifiche: notifier.performChange() e notifier.notify(), che abbiamo già introdotto.

Modifiche su larga scala

Osserviamo questo in un esempio di come è possibile descrivere modifiche su larga scala dove definiamo un oggetto Thingy con alcune utilità matematiche (moltiplica, increment, incrementAndMultiply). Ogni volta che un'utilità viene utilizzata, comunica al sistema che una raccolta di opere include 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 l'oggetto: uno che è un catch-all per i cambiamenti 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 un nuovo elemento:

var thingy = new Thingy(2, 4);

Osservalo e poi apporta qualche modifica. WOW, è divertente. 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 l'opera di "grande cambiamento". Gli osservatori che accettano il "grande cambiamento" riceveranno solo il record di "grandi cambiamenti". Gli osservatori che non riceveranno i cambiamenti di base derivanti dal lavoro svolto dalla "funzione di esecuzione".

Osservazione degli array

Si parla un po' di come osservare le modifiche agli oggetti, ma che succede con gli array? Ottima domanda. Quando qualcuno mi dice "Ottima domanda". Non sento mai la loro risposta perché sono impegnato a congratularmi con me per aver posto una domanda così entusiasmante, ma sto per avanzare. Esistono anche nuovi metodi per lavorare con gli array.

Array.observe() è un metodo che considera se stesso le modifiche su larga scala, ad esempio giunzione, unshift o qualsiasi cosa che ne modifichi implicitamente la lunghezza, come record delle modifiche "splice". Internamente utilizza notifier.performChange("splice",...).

Ecco un esempio in cui osserviamo un "array" di modello e, in modo simile, otteniamo un elenco delle modifiche in caso di 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';
Osservazione degli array

Esibizione

Per valutare l'impatto di O.o() sulle prestazioni di calcolo, puoi considerarlo come 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 richiesto durante le scritture per prestazioni algoritmiche migliori durante le letture.
  3. Il rallentamento a tempo costante delle scritture è accettabile.

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

Per eseguire il controllo sporco è necessario conservare una copia di tutti i dati che stai osservando. Ciò significa che ti viene addebitato un costo di memoria strutturale per eseguire il cosiddetto "sporco controllo", che non si ottiene con O.o(). Il controllo sporco, pur essendo una discreta soluzione stop-gap, è anche un'astrazione fondamentalmente inefficace che può creare complessità superflua per le applicazioni.

Perché? Beh, il controllo sporco deve essere eseguito ogni volta che i dati potrebbero essere cambiati. Non c'è un modo molto efficace per farlo e qualsiasi approccio abbia svantaggi significativi (ad esempio, controllare un intervallo di polling rischia di visualizzare artefatti visivi e condizioni di gara tra problemi di codice). Il controllo sporco richiede anche un registro globale di osservatori, creando rischi di perdita di memoria e costi di eliminazione evitati da O.o().

Diamo un'occhiata ad alcune cifre.

I seguenti test di benchmark (disponibili su GitHub) ci consentono di confrontare il controllo sporco con O.o(). Sono strutturati come grafici di dimensioni dell'insieme di oggetti osservate e di numero di mutazioni. Il risultato generale è che le prestazioni di dirty-check-in sono proporzionali, algoritmicamente, al numero di oggetti osservati, mentre le prestazioni di O.o() sono proporzionali al numero di mutazioni apportate.

Controllo in caso di sporcizia

Controllo del rendimento non corretto

Chrome con Object.observe() attivo

Osserva il rendimento

Polyfilling Object.observe()

Ottimo, quindi O.o() può essere utilizzato in Chrome 36, ma che dire dell'uso in altri browser? Qui troverai tutte le risposte. Observation-JS di Polymer è un polyfill per O.o() che utilizzerà l'implementazione nativa, se presente, ma in caso contrario la riempie e include alcune utili applicazioni di zucchero nella parte superiore. Offre una visione aggregata del mondo che riassume i cambiamenti e fornisce un report di ciò che è cambiato. Due aspetti davvero potenti che mostra:

  1. Puoi osservare i percorsi. Ciò significa che desidero osservare "foo.bar.baz" da un determinato oggetto e che ti dirà quando il valore in quel percorso è cambiato. Se il percorso non è raggiungibile, il valore viene considerato 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 di array. Gli elementi di giunzione degli array sono essenzialmente l'insieme minimo di operazioni di giunzione che devi eseguire su un array per trasformare la versione precedente di un array in quella nuova. Si tratta di un tipo di trasformazione o di una visualizzazione diversa dell'array. Si tratta della quantità minima di lavoro necessaria per passare dallo stato precedente a quello nuovo.

Esempio di modifiche dei report a un array come insieme minimo di giunzioni:

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 dell'associazione dati nei browser che supportano questa 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 un documento di design sul rilevamento dei cambiamenti migliorato di Angular 2.0. Il loro approccio a lungo termine consisterà nell'utilizzare Object.observe() quando arriva nella versione stabile di Chrome, optando per Watchtower.js, il proprio approccio al rilevamento dei cambiamenti fino ad allora. Suuuper è entusiasmante.

Conclusioni

O.o() è un'aggiunta potente alla piattaforma web che puoi usare subito.

Ci auguriamo che col tempo la funzionalità venga aggiunta a più browser, consentendo ai framework JavaScript di migliorare le prestazioni dall'accesso alle funzionalità di osservazione degli oggetti nativi. Gli utenti che hanno come target Chrome dovrebbero poter utilizzare O.o() in Chrome 36 (e versioni successive) e questa funzionalità dovrebbe essere disponibile anche in una versione futura di Opera.

Pertanto, parlane con gli autori dei framework JavaScript su Object.observe() e di come prevedono di utilizzarlo per migliorare le prestazioni dell'associazione dati nelle tue app. Ci sono tempi decisamente entusiasmanti!

Risorse

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