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 se acercan a los modelos de observación para realizar ediciones y actualizaciones. ¿Estás listo para obtener mejoras de rendimiento en las apps que se preocupan por la observación de propiedades?

De acuerdo. Sin más retrasos, me complace anunciar que Object.observe() se encuentra en la versión estable de Chrome 36. [WOOOO. THE CROWD GOES WILD].

Object.observe(), que forma parte de un futuro estándar de ECMAScript, es un método para observar de forma asíncrona los cambios en los objetos de JavaScript… sin necesidad de una biblioteca independiente. Permite que un observador reciba una secuencia de registros de cambios ordenada por tiempo 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:

Se informó el cambio.

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

Eso no significa que no debas usar uno. En el caso de los proyectos grandes con una lógica empresarial complicada, los frameworks con opiniones son inestimables y debes seguir usándolos. Simplifican la orientación de los desarrolladores nuevos, requieren menos mantenimiento de código y, además, imponen patrones sobre cómo realizar tareas comunes. Cuando no lo necesitas, 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 de MV*, O.o() tiene el potencial de proporcionarles algunas mejoras de rendimiento saludables, con una implementación más rápida y sencilla, y manteniendo la misma API. Por ejemplo, el año pasado, Angular descubrió que, en una comparativa en la que se realizaban cambios en un modelo, la verificación de estado no sincronizado tardaba 40 ms por actualización y O.o() tardaba entre 1 y 2 ms por actualización (una mejora de 20 a 40 veces más rápida).

La vinculación de datos sin necesidad de toneladas de código complicado también significa que ya no tienes que consultar los cambios, así que aumenta la duración de la batería.

Si ya usas O.o(), avanza hasta la introducción de las funciones 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 atentos a algunos tipos específicos de cambios:

  • Cambios en los objetos de JavaScript sin procesar
  • Cuándo se agregan, cambian o borran propiedades
  • Cuando los arrays tienen elementos intercalados dentro y fuera de ellos
  • 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 preocupa la separación del control de modelo-vista. HTML es un excelente mecanismo declarativo, pero es completamente estático. Lo ideal es que solo quieras declarar la relación entre tus datos y el DOM, y mantenerlo actualizado. Esto crea una ventaja y te ahorra mucho tiempo escribiendo código realmente repetitivo que solo envía datos desde y hacia 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 en 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 compilando hoy en día.

Al preparar una forma de observar los datos de forma nativa en el navegador, les brindamos a los frameworks de JavaScript (y a las pequeñas bibliotecas de utilidades que escribas) una forma de observar los cambios en los datos de los modelos sin depender de algunos de los trucos lentos que se usan en el mundo actual.

Cómo se ve el mundo en la actualidad

Verificación de estado sucio

¿Dónde has visto la vinculación de datos antes? Bueno, si usas una biblioteca MV* moderna para compilar tus aplicaciones web (p. ej., Angular o Knockout), es probable que estés acostumbrado a vincular los datos del modelo al DOM. A modo de repaso, 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 lista para que nuestros datos y la IU estén siempre 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 el código 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, se actualiza nuestra lista en el DOM. ¿Cómo lo logra Angular? Bueno, detrás de escena hace algo llamado control sucio.

Verificación de estado sucio

La idea básica de la verificación de estado "dirty" es que, cada vez que los datos podrían haber cambiado, la biblioteca debe verificar si realmente cambiaron a través de un resumen o un ciclo de cambio. En el caso de Angular, un ciclo de resumen identifica todas las expresiones registradas para que se supervisen y ver si hay un cambio. Conoce los valores anteriores de un modelo y, si estos cambiaron, se activa un evento de cambio. Para un desarrollador, el principal beneficio es que puedes usar datos de objetos JavaScript sin procesar, que es agradable de usar y tienen una buena composición. La desventaja es que tiene un comportamiento algorítmico malo y es potencialmente muy costoso.

Verificación de estado no sincronizado

El gasto de esta operación es proporcional a la cantidad total de objetos observados. Es posible que tenga que hacer mucha revisión sucia. También es posible que necesites una forma de activar la verificación de estado no sincronizado cuando los datos puedan haber cambiado. Hay muchos trucos ingeniosos {i>frameworks<i} usan para esto. No se sabe si alguna vez será perfecto.

El ecosistema web debería tener más capacidad para innovar y evolucionar 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 de contenedor son el lugar donde un framework crea objetos que contienen los datos. Tienen acceso a los datos y pueden capturar lo que configuras o obtienes y transmitirlo de forma interna. Esto funciona bien. Tiene un rendimiento relativamente alto y un buen comportamiento algorítmico. A continuación, se muestra un ejemplo de objetos de contenedor que usan 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 lo que cambió aquí es proporcional a la cantidad de cosas que cambiaron. Otro problema es que ahora usa este tipo de objeto diferente. En términos generales, debes convertir los datos que obtienes del servidor a estos objetos para que sean observables.

Esto no se compone muy bien con el código JS existente porque la mayoría de los códigos suponen que pueden operar en datos sin procesar. No para estos tipos de objetos especializados.

Introducing Object.observe()

Lo ideal es obtener lo mejor de ambos mundos: una forma de observar datos con compatibilidad con objetos de datos sin procesar (objetos JavaScript normales) si así lo deseamos Y sin necesidad de verificar todo todo el tiempo. Algo con buen comportamiento algorítmico. Algo que se componga bien y se integre en la plataforma. Esta es la belleza de lo que Object.observe() aporta.

Nos permite observar un objeto, mutar propiedades y ver el informe de cambios de lo que cambió. Pero basta de teoría. Veamos código.

Object.observe()

Object.observe() y Object.unobserve()

Supongamos que tenemos un objeto JavaScript simple 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 primer argumento y la devolución de llamada como el segundo:

Object.observe(todoModel, observer);

Empecemos por hacer algunos cambios en nuestro objeto del modelo Todos:

todoModel.label = 'Buy some more milk';

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

Informe de la consola

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

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

Como podemos ver, una vez más, volvemos a obtener un informe de cambios correctamente:

Informe de cambios

Muy bien. ¿Qué pasaría si ahora decidiéramos borrar la propiedad “completed” de nuestro objeto:

delete todoModel.completed;
Completado

Como podemos ver, el informe de cambios que se muestra incluye información sobre la eliminación. Como era de esperar, el valor nuevo de la propiedad ahora no está definido. Por lo tanto, ahora sabemos que puedes averiguar cuándo se agregaron las propiedades. Cuando se hayan borrado. Básicamente, el conjunto de propiedades en un objeto ("nuevo", "eliminado", "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 se puede ver a continuación, cualquier mutación que se realice en el objeto después de que se ejecute ya no generará una lista de registros de cambios.

Mutaciones

Especificar cambios de interés

Así que, analizamos los conceptos básicos para obtener una lista de cambios en un objeto observado. ¿Qué sucede si solo le interesa un subconjunto de cambios que se hicieron a un objeto y no todos? Todos necesitan un filtro de spam. Los observadores pueden especificar solo los tipos de cambios que desean conocer a través de una lista de aceptación. Esto se puede especificar con el tercer argumento de 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, observa que se informa este tipo de cambio:

delete todoModel.label;

Si no especificas una lista de tipos de aceptación para O.o(), se usarán de forma predeterminada los tipos de cambio de objeto "intrínsecos" (add, update, delete, reconfigure, preventExtensions (para cuando no se puede observar que un objeto se vuelve no extensible)).

Notificaciones

O.o() también incluye la noción de notificaciones. No son como esas cosas molestas que obtienes en un teléfono, sino que son bastante útiles. Las notificaciones son similares a las de los Observadores de mutaciones. Ocurren al final de la microtarea. En el contexto del navegador, esto casi siempre estará al final del controlador de eventos actual.

El momento es agradable porque generalmente 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 para definir notificaciones personalizadas para cuando se obtienen o establecen propiedades en un objeto. Consulta 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 datos ("actualización"). Todo lo demás que la implementación del objeto elija informar (notifier.notifyChange()).

Años de experiencia en la plataforma web nos enseñaron que un enfoque síncrono es lo primero que debes probar porque es lo más fácil de entender. El problema es que crea un modelo de procesamiento esencialmente peligroso. Si escribes código y, por ejemplo, actualizas la propiedad de un objeto, no quieres que se actualice la propiedad de ese objeto y que se invite a algún código arbitrario a hacer lo que quiera. No es ideal que se invaliden tus suposiciones mientras ejecutas una función.

Si eres observador, lo ideal sería que no te llamaran si alguien está en el medio de algo. No quieres que se te pida que trabajes en un estado inconsistente del mundo. Realiza muchas más verificaciones de errores. Tratar de tolerar muchas más situaciones malas y, en general, es un modelo difícil de usar. Es más difícil de abordar, pero es un mejor modelo al final del día.

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

Registros de cambios sintéticos

Básicamente, si deseas tener descriptores de acceso o propiedades procesadas, es tu responsabilidad notificar cuando cambian estos valores. 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 los objetos de datos subyacentes. De las propiedades de los datos

Registros de cambios sintéticos

La observación de los descriptores de acceso y las propiedades calculadas se pueden resolver con notifier.notify, otra parte de O.o(). La mayoría de los sistemas de observación desean alguna forma de observar los valores derivados. Hay muchas maneras de hacerlo. O.o no emite ningún juicio sobre la manera "correcta". Las propiedades calculadas deben ser accesores que notifiquen cuando cambie el estado interno (privado).

Una vez más, los desarrolladores web deben esperar que las bibliotecas ayuden a facilitar la notificación y a diversos enfoques para las propiedades procesadas (y a reducir el código estándar).

Vamos a configurar el siguiente ejemplo, que es una clase de círculo. La idea aquí es que tenemos este círculo y hay una propiedad de radio. En este caso, el radio es un accesor y, cuando cambie su valor, notificará por sí mismo que cambió. Se publicará junto con todos los demás cambios en este objeto o en cualquier otro. En términos sencillos, si implementas un objeto, debes tener propiedades sintéticas o procesadas, o debes elegir una estrategia para su funcionamiento. Una vez que lo hagas, se ajustará a todo el sistema.

Omite el código para ver cómo funciona en 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);
  })
}
Consola de registros de cambios sintéticos

Propiedades de acceso

Nota breve sobre las propiedades de acceso Antes mencionamos que solo los cambios de valor son observables para las propiedades de los datos. No es para propiedades calculadas ni para accesores. El motivo es que JavaScript no tiene la noción de cambios en el valor de los accesores. Un accesor es solo una colección de funciones.

Si asignas a un accessor, JavaScript solo invoca la función allí y, desde su punto de vista, no cambió nada. Solo le dio la oportunidad de ejecutarse a un código.

El problema es que, semánticamente, podemos ver nuestra asignación anterior al valor - 5. Debemos saber qué pasó aquí. En realidad, este es un problema que no se puede resolver. En el ejemplo, se muestra por qué. No hay forma de que ningún sistema sepa qué quiere decir esto porque puede ser un código arbitrario. En este caso, puede hacer lo que quiera. Actualiza el valor cada vez que se accede, por lo que preguntar si cambió no tiene mucho sentido.

Cómo observar varios objetos con una devolución de llamada

Otro patrón posible con O.o() es la noción de un solo observador de devolución 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).

Cómo observar varios objetos con una devolución de llamada

Cambios a gran escala

Tal vez estés trabajando en una app muy grande y debas trabajar con cambios a gran escala con frecuencia. Es posible que los objetos deseen describir cambios semánticos más grandes que afectarán a 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 presentamos.

Cambios a gran escala

Veamos esto en un ejemplo de cómo se pueden describir los cambios a gran escala en los que definimos un objeto Thingy con algunas utilidades matemáticas (multiplicar, incrementar, incrementarYmultiplicar). Cada vez que se usa una utilidad, se le indica al sistema que una colección de trabajo 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 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. Definiremos un nuevo elemento Thingy:

var thingy = new Thingy(2, 4);

Obsérvalo y, luego, haz algunos cambios. ¡Qué divertido! ¡Cuántas cositas!

// 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 de ejecución" se considera el trabajo de "cambio importante". Los observadores que acepten "cambio importante" solo recibirán el registro de "cambio importante". Los observadores que no recibirán los cambios subyacentes que resulten del trabajo que realizó la función “ejecutar función”.

Observa arrays

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

Array.observe() es un método que trata los cambios a gran escala a sí mismo, por ejemplo, empalme, un desplazamiento, o cualquier otra cosa que cambie su longitud de forma implícita, como un registro de cambios de "empalme". De forma interna, usa notifier.performChange("splice",...).

Este es un ejemplo en el que observamos un "array" de modelos y, de manera similar, recuperamos 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 pensar en el impacto del 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 cuando (en orden de importancia):

  1. La frecuencia de las operaciones de lectura domina la frecuencia de las operaciones de escritura.
  2. Puedes crear una caché que intercambie la cantidad constante de trabajo que se realiza durante las operaciones de escritura por un mejor rendimiento algorítmico durante las operaciones de lectura.
  3. Se acepta la ralentización constante del tiempo de las operaciones de escritura.

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

La verificación de estado "dirty" requiere mantener una copia de todos los datos que observas. Esto significa que incurrimos en un costo de memoria estructural para la verificación de estado que no se obtiene con O.o(). Si bien la verificación de estado es una solución de emergencia decente, también es una abstracción con fugas fundamentales que puede crear una complejidad innecesaria para las aplicaciones.

¿Por qué? Bueno, la verificación de estado debe ejecutarse cada vez que puedan haber cambiado los datos. Sencillamente, no existe una forma muy sólida de hacerlo, y cualquier enfoque tiene desventajas importantes (p. ej., comprobar el intervalo de sondeo pone en riesgo los artefactos visuales y las condiciones de carrera entre los problemas de código). La verificación de estado no sincronizado también requiere un registro global de observadores, lo que crea peligros de fugas de memoria y costos de desmantelamiento que O.o() evita.

Veamos algunas cifras.

Las siguientes pruebas comparativas (disponibles en GitHub) nos permiten comparar la comprobación no sincronizada 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 comprobación no sincronizada es algorítmicamente proporcional al número de objetos observados, mientras que el rendimiento de O.o() es proporcional a la cantidad de mutaciones que se realizaron.

Verificación sucia

Comprobación no sincronizada del rendimiento

Chrome con Object.observe() activado

Observe el rendimiento

Polyfilling Object.observe()

Genial. Se puede usar O.o() en Chrome 36, pero ¿qué ocurre con el uso 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, de lo contrario, lo rellena e incluye un poco de azúcar útil encima. Ofrece una visión global del mundo que resume los cambios y entrega un informe acerca de ello. Expone dos aspectos muy 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á cuando cambie el valor de 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 dado:

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 las uniones de arrays. Los empalmes de array son básicamente 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 versión nueva. Este es un tipo de transformación o una vista diferente del array. Es la cantidad mínima de trabajo que debes realizar para pasar del estado anterior al nuevo.

Ejemplo de cómo informar cambios en un array como un conjunto mínimo de uniones:

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.observe()

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

Yehuda Katz y Erik Bryn de Ember confirmaron que agregar compatibilidad con O.o() es parte de la hoja de ruta 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 Object.observe() cuando llegue a la versión estable de Chrome y optar por Watchtower.js, su propio enfoque de detección de cambios hasta entonces. Es muy emocionante.

Conclusiones

O.o() es una incorporación potente a la plataforma web que puedes usar hoy mismo.

Esperamos que, con el tiempo, la función llegue a más navegadores, lo que permitirá a los frameworks de JavaScript mejorar el rendimiento a partir del acceso a las capacidades de observación de objetos nativos. Los usuarios 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.

Así que, 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. Sin duda, te esperan momentos emocionantes.

Recursos

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