Rivoluzioni dell'associazione dati con Object.observe()

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. Ok. Senza ulteriori indugi, sono felice di annunciare che Object.observe() è disponibile nella versione stabile di Chrome 36. [WOOOO. LA FOLLA ESULTA].

Object.observe(), 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 ordinata in base al tempo di record di variazione che descrivono l'insieme di modifiche apportate a 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 chiamarlo O.o() o Oooooooo), puoi implementare il binding dati bidirezionale senza bisogno di un framework.

Ciò non significa che non dovresti utilizzarne 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 meno manutenzione del codice e impongono pattern su come svolgere 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, in genere ci riferiamo al monitoraggio di alcuni tipi specifici di modifiche:

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

L'importanza del binding dei dati

Il binding dei dati inizia ad assumere importanza quando ti preoccupi della separazione del controllo del modello e della visualizzazione. L'HTML è un ottimo meccanismo dichiarativo, ma è completamente statico. Idealmente, dovresti solo dichiarare la relazione tra i dati e il DOM e mantenere il DOM aggiornato. In questo modo puoi risparmiare molto tempo scrivendo codice molto ripetitivo che invia semplicemente 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. Questo è abbastanza comune nelle applicazioni a pagina singola che stiamo creando oggi.

Fornendo 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 hack lenti attualmente in uso.

Come è il mondo oggi

Controllo dell'integrità

Dove hai già visto il binding dei dati? 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 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 di base cambiano, il nostro elenco nel DOM viene aggiornato. In che modo Angular ottiene questo risultato? Dietro le quinte, viene eseguita una procedura chiamata controllo degli elementi modificati.

Controllo sporco

L'idea di base del controllo dell'integrità è 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 di digest identifica tutte le espressioni registrate per essere monitorate per verificare se sono presenti modifiche. Conosce i valori precedenti di un modello e, se sono cambiati, viene attivato un evento di modifica. Per uno sviluppatore, il vantaggio principale è che puoi utilizzare i dati dell'oggetto JavaScript non elaborati, che sono piacevoli da usare e si compongono abbastanza bene. Lo svantaggio è che ha un comportamento algoritmico negativo ed è potenzialmente molto costoso.

Controllo sporco.

La spesa di questa operazione è proporzionale al numero totale di oggetti osservati. Potrebbe essere necessario eseguire molti controlli sporchi. Potrebbe anche essere necessario un modo per attivare il controllo dei dati non validi quando i dati potrebbero essere cambiati. Esistono molti trucchi intelligenti utilizzati dai framework per questo scopo. Non è chiaro se la situazione migliorerà.

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 di persistenza automatica (ad es.modifiche permanenti a IndexedDB o localStorage)
  • Oggetti contenitore (Ember, Backbone)

Gli oggetti container sono gli oggetti in cui un framework crea oggetti che all'interno contengono i dati. Hanno accessi ai dati e possono acquisire ciò che imposti o ottieni e trasmettere internamente. Funziona bene. È relativamente performante e ha un 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

Il costo per scoprire cosa è cambiato qui è proporzionale al numero di elementi che sono cambiati. Un altro problema è che ora stai utilizzando questo tipo di oggetto diverso. In generale, devi convertire i dati che ricevi dal server in questi oggetti in modo che siano osservabili.

Questo non si combina particolarmente bene con il codice JS esistente perché la maggior parte del codice presuppone di poter operare su dati non elaborati. Non per questi tipi di 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 (normali oggetti JavaScript) se scegliamo di farlo E senza dover controllare sempre tutto. Qualcosa con un buon comportamento algoritmico. Qualcosa che si compone bene ed è integrato nella piattaforma. Ecco la bellezza di ciò che Object.observe() offre.

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

Object.observe()

Object.observe() e Object.unobserve()

Immaginiamo di avere un semplice oggetto JavaScript 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 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 Todos:

todoModel.label = 'Buy some more milk';

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

Report della console

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

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

Come possiamo vedere, abbiamo di nuovo ricevuto un report sulle modifiche:

Modifica il report.

Ottimo. Cosa succede se ora decidiamo di eliminare la proprietà "completed" 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 scoprire 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 qualsiasi sistema di osservazione, esiste anche un metodo per interrompere l'ascolto delle modifiche. 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 modifica.

Mutazioni

Specificare le variazioni di interesse

Abbiamo quindi esaminato le nozioni di base su come recuperare un elenco di 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 di accettazione per O.o(), per impostazione predefinita vengono utilizzati i tipi di modifica degli oggetti "intrinseci" (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 che ricevi sullo smartphone, ma sono piuttosto utili. Le notifiche sono simili agli osservatori delle mutazioni. Vengono eseguiti al termine della micro-attività. Nel contesto del browser, questo avviene quasi sempre alla fine del gestore dell'evento corrente.

I tempi sono buoni perché in genere una unità di lavoro è stata completata 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 notifier è il 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 riportiamo 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 è che crea un modello di elaborazione fondamentalmente 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 è ideale che le tue ipotesi vengano invalidate nel bel mezzo di una funzione.

Se sei un osservatore, è preferibile che non ti chiamino se qualcuno è impegnato. Non vuoi che ti venga chiesto di lavorare su uno stato incoerente del mondo. Alla fine, dovrai eseguire molti più controlli degli errori. 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 variazione sintetici.

Record delle modifiche sintetiche

In sostanza, se vuoi avere accessor o proprietà calcolate, è tua responsabilità notificare quando questi valori cambiano. È un po' di lavoro in più, ma è progettato come una sorta di funzionalità di prima classe di questo meccanismo e queste notifiche verranno inviate con le altre notifiche degli 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 accessori che invia una notifica quando lo stato interno (privato) cambia.

Anche in questo caso, i web developer dovrebbero aspettarsi che le librerie li aiutino a semplificare le notifiche e vari approcci alle proprietà calcolate (e a ridurre il codice boilerplate).

Configura l'esempio successivo, ovvero una classe cerchio. L'idea è che abbiamo questo cerchio e una proprietà raggio. In questo caso, il raggio è un accessor e, quando il suo valore cambia, invia una notifica che indica che il valore è cambiato. Verrà inviata insieme a tutte le altre modifiche a questo 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. Una volta completata, questa operazione si adatterà al sistema nel suo complesso.

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. Abbiamo accennato in precedenza al fatto che solo le modifiche ai valori sono osservabili per le proprietà di dati. Non per proprietà o accessori calcolati. Il motivo è che JavaScript non ha la nozione di variazioni di valore per gli accessori. Un accessor è semplicemente una raccolta di funzioni.

Se assegni un accessor a JavaScript, la funzione viene invocata lì e dal suo punto di vista non è cambiato nulla. Ha solo dato l'opportunità di eseguire del codice.

Il problema è che semanticamente possiamo guardare l'assegnazione sopra al valore - 5. Dovremmo essere in grado di sapere cosa è successo qui. In realtà 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 un 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 gestire regolarmente modifiche su larga scala. Gli oggetti potrebbero voler descrivere modifiche semantiche più ampie che interessano molte proprietà in modo più compatto (anziché trasmettere tonnellate di modifiche alle proprietà).

O.o() è utile in questo senso grazie a 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 un insieme di attività 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
    });
  }
}

Poi definiamo due osservatori per il nostro oggetto: uno generico per le modifiche e un altro che segnalerà solo 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 dispositivo:

var thingy = new Thingy(2, 4);

Osserva e apporta alcune modifiche. OMG, che bello. Quante 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: "Buona 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. Abbiamo anche nuovi metodi per lavorare con gli array.

Array.observe() è un metodo che tratta le modifiche su larga scala a se stesso, ad esempio splice, unshift o qualsiasi cosa che ne modifichi implicitamente la lunghezza, come un record di modifica "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

Per capire l'impatto sulle prestazioni di calcolo di O.o(), pensa a questa funzione come a una cache di lettura. In generale, una cache è un'ottima scelta quando (in ordine di importanza):

  1. La frequenza delle letture domina la frequenza delle scritture.
  2. Puoi creare una cache che sostituisce la quantità costante di lavoro necessaria durante le scritture con un rendimento algoritmico migliore durante le letture.
  3. Il rallentamento costante delle scritture è accettabile.

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

Il controllo dei dati sporchi richiede di mantenere 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é? Il controllo dell'integrità deve essere eseguito ogni volta che i dati potrebbero essere cambiati. Semplicemente non esiste un modo molto affidabile per farlo e qualsiasi approccio presenta svantaggi significativi (ad esempio, il controllo su un intervallo di polling rischia di creare artefatti visivi e condizioni di gara tra i 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 dell'integrità sono proporzionali in modo algoritmico al numero di oggetti osservati, mentre le prestazioni di O.o() sono proporzionali al numero di mutazioni apportate.

Controllo dell'integrità

Rendimento del controllo sporco

Chrome con Object.observe() attivato

Osservare il rendimento

Polyfilling Object.observe()

Ottimo, quindi O.o() può essere utilizzato in Chrome 36, ma che dire dell'utilizzo in altri browser? Qui troverai tutte le risposte. Observe-JS di Polymer è un polyfill per O.o() che utilizzerà l'implementazione nativa se presente, ma 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 sono:

  1. Puoi osservare i percorsi. Ciò significa che puoi dire, vorrei osservare "foo.bar.baz" da un determinato oggetto e ti diranno 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. Ti parlerà degli splice 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 visualizzazione diversa dell'array. Si tratta del lavoro minimo che devi svolgere per passare dallo stato precedente a quello nuovo.

Esempio di segnalazione delle modifiche a un array come insieme minimo di unioni:

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() è inclusa nella roadmap di Ember a breve termine. Misko Hervy di Angular ha scritto una documentazione di progettazione sul rilevamento delle modifiche migliorato di Angular 2.0. Il loro approccio a lungo termine sarà sfruttare Object.observe() quando verrà implementato in Chrome stabile, optando per Watchtower.js, il loro approccio al rilevamento delle modifiche fino ad allora. Super emozionante.

Conclusioni

O.o() è un'aggiunta potente alla piattaforma web che puoi utilizzare fin da 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 annunci 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 futura release 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 aspettano sicuramente tempi entusiasmanti.

Risorse

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