Revoluciones de la vinculación de datos con Object.observa()

Addy Osmani
Addy Osmani

Introducción

Se acerca una revolución. Hay una nueva incorporación a JavaScript que cambiará todo lo que crees que sabes sobre la vinculación de datos. También cambiará la cantidad de bibliotecas de MVC que abordan los modelos de observación para ediciones y actualizaciones. ¿Todo listo para mejorar el rendimiento de las apps que se preocupan por la observación de propiedades?

Muy bien. De acuerdo. Sin más retrasos, me complace anunciar que Object.observe() llegó a la versión estable de Chrome 36. [impresionante. THE MROWD GOES SALD].

Object.observe(), parte de un futuro estándar de ECMAScript, es un método para observar de forma asíncrona los cambios en los objetos JavaScript... sin la necesidad de una biblioteca separada. Permite que un observador reciba una secuencia ordenada por tiempo de registros de cambios que describe el conjunto de cambios que se produjeron en un conjunto de objetos observados.

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

});

Cada vez que se realiza un cambio, se informa de la siguiente manera:

Se informó el cambio.

Con Object.observe() (me gusta llamarlo O.o() u Oooooooo), puedes implementar la vinculación de datos bidireccional sin la necesidad de un framework.

Eso no significa que no deberías usar uno. Para proyectos grandes con lógica empresarial complicada, los frameworks bien definidos son invaluables y deberías seguir usándolos. Simplifican la orientación de los desarrolladores nuevos, requieren menos mantenimiento del código y, además, imponen patrones para lograr tareas comunes. Cuando no la necesites, puedes usar bibliotecas más pequeñas y enfocadas, como Polymer (que ya aprovecha O.o()).

Incluso si usas mucho un framework o una biblioteca MV*, O.o() tiene el potencial de brindarles algunas mejoras de rendimiento saludables, con una implementación más rápida y sencilla que, al mismo tiempo, mantiene la misma API. Por ejemplo, el año pasado Angular descubrió que, en una comparativa en la que se hacían cambios en un modelo, la verificación sucia tardó 40 ms por actualización y O.o() tardó de 1 a 2 ms por actualización (una mejora de 20 a 40 veces más rápida).

La vinculación de datos sin la necesidad de muchísimos códigos complicados también significa que ya no es necesario consultar en busca de cambios y, por lo tanto, se prolonga la duración de la batería.

Si ya tienes experiencia con O.o(), avanza a la introducción de la función o continúa leyendo para obtener más información sobre los problemas que resuelve.

¿Qué queremos observar?

Cuando hablamos de observación de datos, por lo general nos referimos a estar atento a algunos tipos específicos de cambios:

  • Cambios en objetos JavaScript sin procesar
  • Cuando se agregan, cambian o borran propiedades
  • Cuando los arrays tienen elementos empalmados
  • Cambios en el prototipo del objeto

La importancia de la vinculación de datos

La vinculación de datos comienza a ser importante cuando te importa la separación del control de vista de modelo. HTML es un excelente mecanismo declarativo, pero es completamente estático. Lo ideal es que simplemente quieras declarar la relación entre tus datos y el DOM, y mantener el DOM actualizado. Esto crea ventajas y te ahorra mucho tiempo para escribir código realmente repetitivo que solo envía datos hacia y desde el DOM entre el estado interno de tu aplicación o el servidor.

La vinculación de datos es particularmente útil cuando tienes una interfaz de usuario compleja en la que necesitas conectar relaciones entre múltiples propiedades de tus modelos de datos con múltiples elementos en tus vistas. Esto es bastante común en las aplicaciones de una sola página que estamos creando hoy.

A través de la preparación de una forma de observar los datos de forma nativa en el navegador, los frameworks de JavaScript (y las pequeñas bibliotecas de utilidades que escribes) permiten observar los cambios en los datos de los modelos sin depender de algunos de los hackeos lentos que se usan en la actualidad.

Cómo es el mundo hoy en día

Revisión sucia

¿Dónde viste antes la vinculación de datos? Bueno, si usas una biblioteca MV* moderna para crear apps web (p. ej., Angular o Knockout), probablemente estés acostumbrado a vincular datos del modelo con el DOM. A modo de recordatorio, este es un ejemplo de una app de lista de teléfonos en la que vinculamos el valor de cada teléfono en un array phones (definido en JavaScript) a un elemento de la lista para que nuestros datos y la IU siempre estén sincronizados:

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

y JavaScript para el controlador:

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

Cada vez que cambian los datos del modelo subyacente, nuestra lista en el DOM se actualiza. ¿Cómo lo logra Angular? Bueno, detrás de escena, se hace algo llamado “control de suciedad”.

Verificación sucia

La idea básica de la comprobación sucia es que en cualquier momento que los datos podrían haber cambiado, la biblioteca tiene que revisar si cambió a través de un resumen o un ciclo de cambios. En el caso de Angular, un ciclo de resumen identifica todas las expresiones registradas que se deben observar para ver si hay un cambio. Conoce los valores anteriores de un modelo y, si cambiaron, se activa un evento de cambio. Para un desarrollador, el principal beneficio en este caso es que puede usar datos de objetos JavaScript sin procesar, que son agradables de usar y se componen de forma bastante buena. La desventaja es que tiene un comportamiento algorítmico deficiente y es potencialmente muy costoso.

Verificación sucia.

El gasto de esta operación es proporcional al número total de objetos observados. Es posible que tenga que hacer muchas verificaciones sucias. También es posible que necesites una forma de activar la comprobación no sincronizada cuando los datos podrían haber cambiado. Hay muchos trucos ingeniosos que usan los frameworks para hacerlo. No está claro si alguna vez será perfecto.

El ecosistema web debería tener una mayor capacidad de innovar y desarrollar sus propios mecanismos declarativos, p.ej.,

  • Sistemas de modelos basados en restricciones
  • Sistemas de persistencia automática (p. ej., cambios persistentes en IndexedDB o localStorage)
  • Objetos de contenedor (Ember, Backbone)

Los objetos contenedores son el lugar donde un framework crea objetos que, en su interior, contienen los datos. Tienen acceso a los datos y pueden capturar lo que estableces o lo que recibes y transmitirlo de manera interna. Esto funciona bien. Tiene un buen rendimiento y un buen comportamiento algorítmico. A continuación, se muestra un ejemplo de objetos de contenedor con 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

El gasto de descubrir qué cambió aquí es proporcional a la cantidad de cosas que cambiaron. Otro problema es que estás usando este tipo de objeto diferente. En términos generales, hay que convertir los datos que se reciben del servidor en estos objetos para que sean observables.

Esto no se compone particularmente bien con el código JS existente, ya que la mayor parte del código da por sentado que puede operar con datos sin procesar. No sirve para estos tipos especializados de objetos.

Introducing Object.observe()

Idealmente, lo que queremos es lo mejor de ambos mundos: una forma de observar los datos con compatibilidad para objetos de datos sin procesar (objetos regulares de JavaScript) si elegimos usar el operador Y sin la necesidad de revisar todo en segundo plano todo el tiempo. Algo con un buen comportamiento algorítmico. Algo que se componga bien y se integre en la plataforma. Esta es la belleza de lo que ofrece Object.observe().

Nos permite observar un objeto, cambiar propiedades y ver el informe de cambios de lo que cambió. Pero suficiente sobre la teoría, veamos algo de código.

Object.observe()

Object.observa() y Object.unobserva()

Imaginemos que tenemos un objeto JavaScript convencional que representa un modelo:

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

Luego, podemos especificar una devolución de llamada para cada vez que se realicen mutaciones (cambios) en el objeto:

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

Luego, podemos observar estos cambios usando O.o(), pasando el objeto como nuestro primer argumento y la devolución de llamada como nuestro segundo:

Object.observe(todoModel, observer);

Empecemos por realizar algunos cambios en nuestro objeto de modelo Todos:

todoModel.label = 'Buy some more milk';

Si observamos la consola, obtenemos información útil. Sabemos qué propiedad cambió, cómo se cambió y cuál es el valor nuevo.

Informe de la consola

¡Bravo! ¡Adiós, control! Tu lápida debería estar tallada en Comic Sans. Cambiemos otra propiedad. Esta vez completeBy:

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

Como podemos ver, recibimos un informe de cambios de nuevo con éxito:

Informe de cambios.

Muy bien. ¿Qué pasaría si ahora decidimos borrar la propiedad "completed" de nuestro objeto?

delete todoModel.completed;
Completada

Como podemos ver, el informe de cambios que se muestra incluye información sobre la eliminación. Como se esperaba, el valor nuevo de la propiedad ahora es indefinido. Ahora sabemos que puedes averiguar cuando se agregaron propiedades. Cuando se hayan borrado Básicamente, es el conjunto de propiedades de un objeto ("nuevo", "borrado", "reconfigurado") y su prototipo cambia (proto).

Como en cualquier sistema de observación, también existe un método para dejar de escuchar los cambios. En este caso, es Object.unobserve(), que tiene la misma firma que O.o(), pero se puede llamar de la siguiente manera:

Object.unobserve(todoModel, observer);

Como podemos ver a continuación, cualquier mutación realizada al objeto después de que se haya ejecutado ya no dará como resultado una lista de registros de cambios.

Mutaciones

Cómo especificar cambios de interés

Vimos los conceptos básicos detrás de cómo obtener una lista de cambios en un objeto observado. ¿Qué sucede si te interesa solo un subconjunto de cambios que se realizaron en un objeto en lugar de todos? Todos deben tener un filtro de spam. Los observadores solo pueden especificar los tipos de cambios que deseen recibir a través de una lista de aceptación. Esto se puede especificar usando el tercer argumento para O.o() de la siguiente manera:

Object.observe(obj, callback, optAcceptList)

Veamos un ejemplo de cómo se puede usar:

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

Sin embargo, si ahora borramos la etiqueta, ten en cuenta que este tipo de cambio sí se informa:

delete todoModel.label;

Si no especificas una lista de tipos de aceptación en O.o(), el valor predeterminado es los tipos de cambio de objeto "intrínsecos" (add, update, delete, reconfigure, preventExtensions (para cuando un objeto que se vuelve no extensible no es observable).

Notificaciones

O.o() también incluye la noción de notificaciones. No son como las cosas molestas que ves en un teléfono, sino más bien útiles. Las notificaciones son similares a Mutation Observers. Suceden al final de la microtarea. En el contexto del navegador, esto casi siempre va al final del controlador de eventos actual.

El tiempo es bueno porque, por lo general, se termina una unidad de trabajo y ahora los observadores pueden hacer su trabajo. Es un buen modelo de procesamiento por turnos.

El flujo de trabajo para usar un notificador se ve de la siguiente manera:

Notificaciones

Veamos un ejemplo de cómo se pueden usar los notificadores en la práctica a fin de definir notificaciones personalizadas para el momento en que se obtienen o configuran las propiedades de un objeto. No te pierdas los comentarios aquí:

// 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);
Consola de notificaciones

Aquí informamos cuando cambia el valor de las propiedades de los datos ("update"). Todos los demás datos que la implementación del objeto decida informar (notifier.notifyChange()).

Gracias a los años de experiencia en la plataforma web, lo primero que se prueba es un enfoque síncrono porque es lo más fácil de entender. El problema es que crea un modelo de procesamiento peligroso. Si escribes código y dices: actualizar la propiedad de un objeto, no querrás que una situación en la que se actualiza la propiedad de ese objeto haya invitado un código arbitrario para hacer lo que quiera. No es ideal que se invaliden tus suposiciones mientras ejecutas una función en medio de ella.

Si eres observador, lo ideal sería que no te llamen si alguien está en medio de algo. No quieres que te pidan que te hagan trabajar en un estado del mundo inconsistente. Terminar por hacer muchas más verificaciones de errores Tratar de tolerar muchas más situaciones malas y, en general, es un modelo difícil con el que trabajar. Es más difícil manejar el modo asíncrono, pero, al final del día, es un modelo mejor.

La solución a este problema son los registros de cambio sintéticos.

Registros de cambios sintéticos

Básicamente, si deseas tener descriptores de acceso o propiedades calculadas, es tu responsabilidad notificar cuando estos valores cambien. Implica un poco de trabajo adicional, pero está diseñado como una especie de función de primera clase de este mecanismo, y estas notificaciones se entregarán con el resto de las notificaciones de objetos de datos subyacentes. De las propiedades de datos.

Registros de cambios sintéticos

La observación de descriptores de acceso y las propiedades calculadas se puede resolver con notifier.notify, otra parte de O.o(). La mayoría de los sistemas de observación desean alguna forma de observar valores derivados. Hay muchas maneras de hacerlo. O.o no juzga la forma “correcta”. Las propiedades procesadas deben ser descriptores de acceso que notify cuando cambia el estado interno (privado).

Una vez más, los desarrolladores web deben esperar que las bibliotecas faciliten las notificaciones y diversos enfoques para las propiedades procesadas (y reduzcan el código estándar).

Configuremos el siguiente ejemplo, que es una clase circular. La idea es que tenemos este círculo y una propiedad de radio. En este caso, el radio es un descriptor de acceso y, cuando cambia su valor, se notificará por sí mismo que el valor cambió. Esto se entregará con todos los demás cambios que se realicen en este o en cualquier otro objeto. En esencia, si implementas un objeto, quieres tener propiedades sintéticas o computadas, o debes elegir una estrategia sobre cómo funcionará esto. Una vez que lo hagas, se adaptará a todo tu sistema.

Omite el código para ver cómo funciona en Herramientas para desarrolladores.

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);
  })
}
Consola de registros de cambio sintético

Propiedades del descriptor de acceso

Una aclaración breve sobre las propiedades de los descriptores de acceso. Mencionamos que solo los cambios en los valores son observables para las propiedades de los datos. No para propiedades ni descriptores de acceso calculados. La razón es que JavaScript realmente no tiene la noción de cambios en el valor para los descriptores de acceso. Un descriptor de acceso es solo una colección de funciones.

Si asignas a un descriptor de acceso, JavaScript solo invoca la función allí y, desde su punto de vista, no cambió nada. ya que dio la oportunidad de ejecutarse a algo de código.

Desde el punto de vista semántico, el problema es que podemos ver nuestra asignación anterior al valor - 5. Deberíamos poder saber qué pasó aquí. Este es un problema que no se puede resolver. El ejemplo demuestra por qué. Realmente no hay forma de que ningún sistema sepa qué significa esto porque puede ser un código arbitrario. En este caso, puede hacer lo que quiera. Actualiza el valor cada vez que se accede a él, por lo que preguntar si cambió no tiene mucho sentido.

Observa varios objetos con una devolución de llamada

Otro patrón posible con O.o() es la noción de un solo observador de devoluciones de llamada. Esto permite que se use una sola devolución de llamada como “observador” para muchos objetos diferentes. La devolución de llamada proporcionará el conjunto completo de cambios a todos los objetos que observe al "final de la microtarea" (ten en cuenta la similitud con Mutation Observers).

Observa varios objetos con una devolución de llamada

Cambios a gran escala

Tal vez estés trabajando en una app muy grande y tengas que trabajar regularmente con cambios a gran escala. Es posible que los objetos quieran describir cambios semánticos más grandes que afectarán muchas propiedades de una manera más compacta (en lugar de transmitir toneladas de cambios de propiedades).

O.o() ayuda con esto en forma de dos utilidades específicas: notifier.performChange() y notifier.notify(), que ya vimos.

Cambios a gran escala

Veamos esto en un ejemplo de cómo pueden describirse los cambios a gran escala en el que definimos un objeto Thingy con algunas utilidades matemáticas (multiplicar, incrementar, incrementoAndMultiply). Cada vez que se usa una utilidad, le indica al sistema que una colección de trabajos comprende un tipo específico de cambio.

Por ejemplo: 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
    });
  }
}

Luego, definimos dos observadores para nuestro objeto: uno que es genérico para los cambios y otro que solo informa sobre los tipos de aceptación específicos que definimos (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);
}

Ahora podemos comenzar a jugar con este código. Definamos un nuevo Thingy:

var thingy = new Thingy(2, 4);

Revísala y, luego, realiza algunos cambios. ¡Qué divertido! ¡Tantas cosas!

// 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 }
Cambios a gran escala

Todo lo que se encuentra dentro de la "función realizar" se considera el trabajo de un "gran cambio". Los observadores que acepten el "gran cambio" solo recibirán el registro "grande cambio". Los observadores que no reciban los cambios subyacentes resultantes del trabajo que hizo "perform function" (realizar función)

Observa los arrays

Hablamos por un tiempo sobre la observación de cambios en los objetos, pero ¿qué sucede con los arrays? Muy buena pregunta. Cuando alguien me dice: "Excelente pregunta". Nunca escucho su respuesta porque estoy ocupada al felicitarme por hacer una pregunta tan buena, pero me hondo. También tenemos nuevos métodos para trabajar con arrays.

Array.observe() es un método que trata los cambios a gran escala hacia sí mismo, por ejemplo, el empalme, el desplazamiento o cualquier otro cambio que modifique su longitud de manera implícita, como un registro de cambios de "empaldo". Usa notifier.performChange("splice",...) de forma interna.

Este es un ejemplo en el que observamos un “array” de modelo y, de manera similar, obtenemos una lista de cambios cuando hay algún cambio en los datos subyacentes:

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';
Observa los arrays

Rendimiento

La forma de entender el impacto en el rendimiento computacional de O.o() es pensar en él como una caché de lectura. En términos generales, una caché es una excelente opción en los siguientes casos (en orden de importancia):

  1. La frecuencia de las lecturas domina la de las escrituras.
  2. Puedes crear una caché que intercambie la cantidad constante de trabajo involucrado durante las escrituras por un mejor rendimiento algorítmico durante las lecturas.
  3. Se acepta la demora constante en el tiempo de las operaciones de escritura.

O.o() está diseñado para casos de uso como el 1).

La verificación sucia requiere conservar una copia de todos los datos que estás observando. Esto significa que se genera un costo de memoria estructural por la comprobación sucia, pero no se consigue con O.o(). Esta comprobación, aunque es una solución aceptable, también es una abstracción fundamentalmente con fugas que puede crear complejidad innecesaria para las aplicaciones.

¿Por qué? Bueno, la verificación no sincronizada se debe ejecutar en cualquier momento que los datos puedan haber cambiado. Sencillamente, no hay una manera muy sólida de hacerlo, y cualquier enfoque al respecto tiene desventajas significativas (p. ej., verificar un intervalo de sondeo implica el riesgo de artefactos visuales y condiciones de carrera entre problemas de código). La comprobación sucia también requiere un registro global de observadores, lo que crea riesgos de fuga de memoria y costos de desmontaje que O.o() evita.

Veamos algunas cifras.

Las siguientes pruebas comparativas (disponibles en GitHub) nos permiten comparar el control sucio con O.o(). Se estructuran como gráficos de Observed-Object-Set-Size frente a Number-Of-Mutations. El resultado general es que el rendimiento de la verificación sucia es proporcional al algoritmo proporcional a la cantidad de objetos observados, mientras que el rendimiento de O.o() es proporcional a la cantidad de mutaciones que se realizaron.

Revisión sucia

Verificación sucia de rendimiento

Chrome con Object.observa() activado

Observa el rendimiento

Objeto de polifilling.observa()

Excelente. Así que O.o() puede usarse en Chrome 36, pero ¿qué pasa si lo usas en otros navegadores? Tenemos lo que necesitas Observe-JS de Polymer es un polyfill para O.o() que usará la implementación nativa si está presente, pero lo completará como polyfill e incluirá un poco de azúcar útil en la parte superior. Ofrece una vista global del mundo que resume los cambios y entrega un informe de lo que ha cambiado. Expone dos elementos realmente importantes:

  1. Puedes observar rutas. Esto significa que puedes decir que me gustaría observar “foo.bar.baz” de un objeto determinado y te dirán cuándo cambió el valor en esa ruta. Si no se puede acceder a la ruta, se considera que el valor no está definido.

Ejemplo de observación de un valor en una ruta desde un objeto determinado:

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. Te informará sobre los empalmes de arrays. Básicamente, los empalmes de array son el conjunto mínimo de operaciones de empalme que tendrás que realizar en un array para transformar la versión anterior del array en la nueva. Este es un tipo de transformación o una vista diferente del array. Es la cantidad mínima de trabajo que debes hacer para pasar del estado anterior al nuevo.

Ejemplo de informes sobre cambios en un array como un conjunto mínimo de empalmes:

var arr = [0, 1, 2, 4];

var observer = new ArrayObserver(arr);
observer.open(function(splices) {
  // respond to changes to the elements of arr.
  splices.forEach(function(splice) {
    splice.index; // index position that the change occurred.
    splice.removed; // an array of values representing the sequence of elements which were removed
    splice.addedCount; // the number of elements which were inserted.
  });
});

Frameworks y Object.observa()

Como se mencionó, O.o() dará a los frameworks y las bibliotecas una gran oportunidad para mejorar el rendimiento de su vinculación de datos en navegadores que admiten la función.

Yehuda Katz y Erik Bryn de Ember confirmaron que agregar soporte para O.o() está en el plan de actuación a corto plazo de Ember. Misko Hervy, de Angular, escribió un documento de diseño sobre la detección de cambios mejorada de Angular 2.0. Su enfoque a largo plazo será aprovechar la función Object.observe() cuando llegue a la versión estable de Chrome y habilitar Watchtower.js, su propio enfoque de detección de cambios hasta ese momento. Emocionante.

Conclusiones

O.o() es una poderosa incorporación a la plataforma web que puedes utilizar hoy en día.

Esperamos que, con el tiempo, la función esté disponible en más navegadores, lo que permitirá que los frameworks de JavaScript obtengan un aumento en el rendimiento del acceso a las capacidades de observación de objetos nativos. Aquellos que se orienten a Chrome deberían poder usar O.o() en Chrome 36 (y versiones posteriores), y la función también debería estar disponible en una versión futura de Opera.

Por lo tanto, habla con los autores de los frameworks de JavaScript sobre Object.observe() y cómo planean usarlo para mejorar el rendimiento de la vinculación de datos en tus apps. Definitivamente se esperan momentos emocionantes.

Recursos

Agradecimientos a Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan y Vivian Cromwell por sus aportes y opiniones.