Revoluções de vinculação de dados com Object.observe()

Addy Osmani
Addy Osmani

Introdução

Uma revolução está chegando. Há uma nova adição ao JavaScript que mudará tudo o que você acha que sabe sobre a vinculação de dados. Isso também vai mudar a forma como muitas de suas bibliotecas MVC observam os modelos para edições e atualizações. Tudo pronto para melhorar o desempenho dos apps que se preocupam com a observação de propriedades?

Certo. Sem mais atrasos, fico feliz em anunciar que o Object.observe() chegou à versão estável do Chrome 36. [SOOOOO! THE CROWD GOES WILD].

Object.observe(), parte de um futuro padrão ECMAScript, é um método de observação assíncrona de alterações em objetos JavaScript sem a necessidade de uma biblioteca separada. Ele permite que um observador receba uma sequência ordenada por tempo de registros de mudanças que descrevem o conjunto de mudanças que ocorreram em um 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);
    });

});

Sempre que uma mudança é feita, ela é informada:

Alteração informada.

Com o Object.observe(), que gosto de chamar de O.o() ou Oooooooo, você pode implementar a vinculação de dados bidirecional sem a necessidade de um framework.

Isso não quer dizer que você não deve usar um. Para grandes projetos com lógica de negócios complicada, estruturas opinativas são inestimáveis e você deve continuar a usá-las. Eles simplificam a orientação dos novos desenvolvedores, exigem menos manutenção de código e impõem padrões sobre como realizar tarefas comuns. Quando você não precisar de um, é possível usar bibliotecas menores e mais focadas, como a Polymer (que já usa o O.o()).

Mesmo que você use intensamente uma biblioteca ou biblioteca MV*, o O.o() tem o potencial de oferecer melhorias de desempenho saudáveis, com uma implementação mais rápida e simples, mantendo a mesma API. Por exemplo, o Angular descobriu no ano passado que, em um comparativo de mercado em que as alterações eram feitas em um modelo, a verificação suja demorava 40 ms por atualização e O.o() levava de 1 a 2 ms por atualização (uma melhoria de 20 a 40 vezes mais rápida).

A vinculação de dados sem a necessidade de muitos códigos complicados também significa que você não precisa mais procurar mudanças, o que aumenta a duração da bateria.

Se você já vende o O.o(), pule para a introdução do recurso ou leia mais sobre os problemas que ele resolve.

O que queremos observar?

Quando se trata de observação de dados, normalmente estamos nos referindo a acompanhar alguns tipos específicos de alterações:

  • Alterações em objetos JavaScript brutos
  • Quando propriedades são adicionadas, alteradas ou excluídas
  • Quando as matrizes têm elementos adicionados e removidos delas
  • Alterações no protótipo do objeto

A importância da vinculação de dados

A vinculação de dados começa a se tornar importante quando você se preocupa com a separação entre modelo e controle de visualização. O HTML é um ótimo mecanismo declarativo, mas é completamente estático. O ideal é declarar a relação entre seus dados e o DOM e manter o DOM atualizado. Isso gera alavancagem e economiza muito tempo escrevendo código bastante repetitivo que apenas envia dados de e para o DOM entre o estado interno do seu aplicativo ou o servidor.

A vinculação de dados é especialmente útil quando você tem uma interface do usuário complexa em que é preciso conectar relações entre várias propriedades nos modelos de dados com vários elementos nas visualizações. Isso é muito comum nos aplicativos de página única que estamos criando hoje.

Ao criar uma maneira de observar nativamente dados no navegador, fornecemos estruturas JavaScript (e pequenas bibliotecas de utilitários que você escreve) uma maneira de observar alterações nos dados de modelo sem depender de algumas das invasões lentas que o mundo usa hoje.

Como é o mundo hoje

Verificação de itens

Onde você já viu a vinculação de dados antes? Se você usa uma biblioteca MV* moderna para criar apps da Web (por exemplo, Angular, Knockout), provavelmente já está acostumado a vincular dados de modelo ao DOM. Para relembrar, aqui está um exemplo de um app de lista de telefones em que estamos vinculando o valor de cada smartphone em uma matriz phones (definida em JavaScript) a um item da lista para que nossos dados e a interface estejam sempre 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>

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

Sempre que os dados do modelo subjacentes mudam, nossa lista no DOM é atualizada. Como o Angular faz isso? Bem, nos bastidores ele faz algo chamado verificação suja.

Verificação incorreta

A ideia básica da verificação suja é que, sempre que os dados podem ter sido alterados, a biblioteca precisa verificar se houve alterações por meio de um resumo ou de um ciclo de alterações. No caso do Angular, um ciclo de resumo identifica todas as expressões registradas para serem observadas para ver se há uma mudança. Ela sabe os valores anteriores de um modelo e, caso eles tenham mudado, um evento de alteração é acionado. Para um desenvolvedor, o principal benefício é que você pode usar dados brutos de objetos JavaScript, o que é agradável de usar e compõe bem. A desvantagem é que ela tem mau comportamento algorítmico e é potencialmente muito cara.

A verificação não foi feita.

As despesas dessa operação são proporcionais ao número total de objetos observados. talvez eu precise fazer várias verificações. Além disso, pode ser necessário acionar a verificação suja quando os dados talvez tenham sido alterados. Existem muitas estruturas de truques inteligentes usam para isso. Não está claro se isso será perfeito.

O ecossistema da Web deve ter mais capacidade de inovar e desenvolver os próprios mecanismos declarativos, como

  • Sistemas de modelos baseados em restrições
  • Sistemas de persistência automática (por exemplo, alterações persistentes no IndexedDB ou localStorage)
  • Objetos de contêiner (Ember, Backbone)

Os objetos contêiner são onde um framework cria objetos que, no interior, armazenam os dados. Eles têm acessores aos dados e podem capturar o que você define ou recebe e transmite internamente. Isso funciona bem. É relativamente eficiente e tem bom comportamento algorítmico. Um exemplo de objetos de contêiner que usam Ember pode ser encontrado abaixo:

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

A despesa de descobrir o que mudou aqui é proporcional ao número de coisas que mudaram. Outro problema é que agora você está usando esse tipo diferente de objeto. De modo geral, você precisa converter os dados recebidos do servidor em esses objetos para que sejam observáveis.

Isso não funciona muito bem com o código JS existente porque a maioria dos códigos pressupõe que ele pode operar em dados brutos. Não para esses tipos especializados de objetos.

Introducing Object.observe()

O ideal é o melhor dos dois mundos: uma maneira de observar os dados com suporte para objetos de dados brutos (objetos JavaScript normais) se escolhermos AND sem a necessidade de verificar tudo o tempo todo. Algo com bom comportamento algorítmico. Algo que componha bem e seja incorporado à plataforma. Essa é a beleza do que Object.observe() traz para o mercado.

Ela nos permite observar um objeto, modificar propriedades e ver o relatório de alterações sobre o que mudou. Mas chega de falar sobre a teoria e vamos conferir alguns códigos.

Object.observe()

Object.observe() e Object.unobserve()

Vamos imaginar que temos um objeto JavaScript simples simples que representa um modelo:

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

Assim, podemos especificar um callback para sempre que mutações (mudanças) forem feitas no 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
  });
}

Podemos observar essas mudanças usando O.o(), transmitindo o objeto como o primeiro argumento e o callback como o segundo:

Object.observe(todoModel, observer);

Vamos começar a fazer algumas mudanças no objeto de modelo Todos:

todoModel.label = 'Buy some more milk';

Analisando o console, encontramos algumas informações úteis. Sabemos qual propriedade foi alterada, como ela foi alterada e qual é o novo valor.

Relatório do console

Uhu! Adeus, verificação suja! Sua lápide deve ser esculpida em Comic Sans. Vamos alterar outra propriedade. Desta vez, completeBy:

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

Como podemos ver, mais uma vez recebemos um relatório de alterações:

Alterar relatório.

Ótimo. E se agora decidirmos excluir a propriedade "completed" de nosso objeto:

delete todoModel.completed;
Concluído

Como podemos ver, o relatório das alterações retornadas inclui informações sobre a exclusão. Como esperado, o novo valor da propriedade agora está indefinido. Agora sabemos que você pode conferir quando propriedades foram adicionadas. Quando forem excluídas. Basicamente, o conjunto de propriedades em um objeto ("novo", "excluído", "reconfigurado") e o respectivo protótipo mudando (proto).

Como em qualquer sistema de observação, também existe um método para parar de detectar as mudanças. Nesse caso, é Object.unobserve(), que tem a mesma assinatura que O.o(), mas pode ser chamado da seguinte maneira:

Object.unobserve(todoModel, observer);

Como podemos ver abaixo, mutações feitas no objeto depois de serem executadas não resultam mais em uma lista de registros de alteração retornada.

Mutações

Especificar alterações de interesse

Aprendemos as noções básicas por trás de como recuperar uma lista de alterações em um objeto observado. E se você tiver interesse em apenas um subconjunto de mudanças feitas em um objeto, e não em todas elas? Todos precisam de um filtro de spam. Bem, os observadores podem especificar apenas esses tipos de mudanças que querem ouvir por meio de uma lista de aceitação. Isso pode ser especificado usando o terceiro argumento para O.o(), da seguinte maneira:

Object.observe(obj, callback, optAcceptList)

Vamos conferir um exemplo de como isso pode ser usado:

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

No entanto, se agora excluirmos o rótulo, esse tipo de alteração será relatado:

delete todoModel.label;

Se você não especificar uma lista de tipos de aceitação como O.o(), o padrão será os tipos de mudança de objeto "intrínsecos" (add, update, delete, reconfigure, preventExtensions, para quando um objeto se tornar não extensível não for observável).

Notificações

O.o() também acompanha a noção de notificações. Eles não se parecem com as coisas irritantes que você vê em um celular, mas são bastante úteis. As notificações são semelhantes aos Observadores de Mutação. Elas acontecem no final da microtarefa. No contexto do navegador, ele quase sempre vai estar no final do manipulador de eventos atual.

O momento é bom porque geralmente uma unidade de trabalho é concluída e agora os observadores podem fazer o trabalho. É um bom modelo de processamento baseado em turnos.

O fluxo de trabalho para usar um notificador é semelhante ao seguinte:

Notificações

Vejamos um exemplo de como os notificadores podem ser usados na prática para definir notificações personalizadas para quando as propriedades em um objeto são obtidas ou definidas. Fique de olho nos comentários aqui:

// 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 de notificações

Aqui, informamos quando o valor das propriedades de dados é alterado ("atualização"). Qualquer outra coisa que a implementação do objeto escolher informar (notifier.notifyChange()).

Anos de experiência na plataforma da Web nos ensinaram que a abordagem síncrona é a primeira coisa que você tenta, porque é a mais fácil de entender. O problema é que isso cria um modelo de processamento fundamentalmente perigoso. Se você estiver escrevendo código e disser para atualizar a propriedade de um objeto, não seria ideal que uma situação de atualização da propriedade desse objeto pudesse convidar um código arbitrário para fazer o que quisesse. Não é o ideal que suas suposições sejam invalidadas enquanto você executa uma função.

Se você é um observador, o ideal é que não seja chamado se alguém estiver no meio de algo. Você não quer ser solicitado a trabalhar em um estado inconsistente do mundo. Acabo fazendo muitas outras verificações de erros. Tentar tolerar muito mais situações ruins e, geralmente, é um modelo difícil de se trabalhar. É mais difícil lidar com o assíncrono, mas, no fim das contas, é um modelo melhor.

A solução para esse problema são os registros de alterações sintéticos.

Registros de alteração sintéticos

Basicamente, se você quiser ter acessadores ou propriedades computadas, é sua responsabilidade notificar quando esses valores mudarem. É um pouco mais trabalhoso, mas foi projetado como uma espécie de recurso de primeira classe desse mecanismo, e essas notificações serão entregues com o restante das notificações de objetos de dados subjacentes. Usando propriedades de dados.

Registros de alteração sintéticos

A observação de acessadores e propriedades computadas pode ser resolvida com notifier.notify - outra parte de O.o(). A maioria dos sistemas de observação quer alguma forma de observar valores derivados. Há muitas maneiras de fazer isso. O.o não faz julgamentos sobre a maneira "certa". As propriedades computadas precisam ser acessadores que notify quando o estado interno (particular) muda.

Novamente, os webdevs devem esperar que as bibliotecas ajudem a facilitar a notificação e várias abordagens para propriedades computadas (e reduzir o código boilerplate).

Vamos definir o próximo exemplo, que é uma classe de círculo. A ideia aqui é que temos este círculo e há uma propriedade de raio. Nesse caso, o raio é um acessador e, quando o valor muda, ele é notificado de que o valor mudou. Ele será entregue com todas as outras alterações neste ou em qualquer outro objeto. Se você estiver implementando um objeto e quiser ter propriedades sintéticas ou computadas, será necessário escolher uma estratégia de como isso vai funcionar. Depois de fazer isso, isso se encaixa no seu sistema como um todo.

Pule o código para que isso funcione no 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 de registros de alteração sintéticos

Propriedades do acessador

Uma observação rápida sobre as propriedades do acessador. Mencionamos anteriormente que apenas as mudanças de valor são observáveis para propriedades de dados. Isso não se aplica a propriedades ou acessadores computados. Isso porque o JavaScript não tem a noção de alterações de valor para os acessadores. Um acessador é apenas um conjunto de funções.

Se você atribuir a um acessador, o JavaScript apenas invocará a função e, do ponto de vista, nada será alterado. Isso deu a alguns códigos a oportunidade de ser executado.

O problema é semanticamente, podemos observar nossa atribuição acima ao valor - 5 para ele. Precisamos saber o que aconteceu aqui. Este problema não pode ser resolvido. O exemplo demonstra o motivo. Realmente não há como nenhum sistema saber o que isso significa, pois pode ser um código arbitrário. Nesse caso, ele pode fazer o que quiser. O valor é atualizado sempre que é acessado, então não faz muito sentido perguntar se ele mudou.

Como observar vários objetos com um callback

Outro padrão possível com O.o() é a noção de um único observador de callback. Isso permite que um único callback seja usado como um "observador" para muitos objetos diferentes. O callback será entregue com o conjunto completo de mudanças para todos os objetos observados no "fim da microtarefa" (observe a semelhança com os observadores de mutação).

Como observar vários objetos com um callback

Mudanças em grande escala

Talvez você esteja trabalhando em um app muito grande e tenha que trabalhar regularmente com mudanças em grande escala. Os objetos podem querer descrever mudanças semânticas maiores que afetarão muitas propriedades de forma mais compacta (em vez de transmitir toneladas de mudanças de propriedade).

O.o() ajuda com isso usando dois utilitários específicos: notifier.performChange() e notifier.notify(), que já apresentamos.

Mudanças em grande escala

Vamos conferir isso em um exemplo de como mudanças em grande escala podem ser descritas em que definimos um objeto Thingy com alguns utilitários de matemática (multiply, increment, incrementAndMultiply). Sempre que um utilitário é usado, ele informa ao sistema que uma coleção de obras compreende um tipo específico de mudança.

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

Em seguida, definimos dois observadores para nosso objeto: um que é abrangente para as alterações e outro que só informará sobre tipos de aceitação 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);
}

Agora podemos começar a mexer com esse código. Vamos definir um novo Thingy:

var thingy = new Thingy(2, 4);

Observe e, em seguida, faça algumas alterações. Uau, que divertido. Muitas coisas!

// 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 }
Mudanças em grande escala

Tudo o que está dentro da "função de desempenho" é considerado o trabalho de "grande mudança". Os observadores que aceitam "grande mudança" recebem apenas o registro de "grande mudança". Observadores que não receberão as alterações subjacentes resultantes do trabalho que "realizaram a função".

Como observar matrizes

Já falamos sobre como observar mudanças em objetos há algum tempo, mas e quanto às matrizes? Ótima pergunta. Quando alguém me diz: “Ótima pergunta”. Nunca ouço a resposta porque estou ocupada me parabenizando por fazer uma pergunta tão boa, mas me desloquei. Também temos novos métodos para trabalhar com matrizes.

Array.observe() é um método que trata mudanças em grande escala em si mesmo, por exemplo, união, unshift ou qualquer coisa que mude implicitamente o comprimento, como um registro de alterações de "união". Internamente, ele usa notifier.performChange("splice",...).

Confira um exemplo em que observamos uma "matriz" de modelo e, da mesma forma, recebemos uma lista de alterações quando há alguma mudança nos dados subjacentes:

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';
Como observar matrizes

Desempenho

Para entender o impacto do desempenho computacional de O.o(), pense nele como um cache de leitura. De modo geral, um cache é uma ótima opção quando (em ordem de importância):

  1. A frequência de leituras domina a frequência de gravações.
  2. Você pode criar um cache que troca a quantidade constante de trabalho envolvido durante as gravações por um desempenho algorítmico melhor durante as leituras.
  3. A lentidão constante das gravações é aceitável.

O.o() foi projetado para casos de uso como 1).

A verificação suja requer manter uma cópia de todos os dados que você está observando. Isso significa que há um custo de memória estrutural para a verificação suja que não ocorre com O.o(). A verificação suja, embora seja uma boa solução de interrupção, também é uma abstração fundamentalmente vazada que pode criar complexidade desnecessária para os aplicativos.

Por quê? Bem, a verificação suja precisa ser executada sempre que os dados talvez tenham mudado. Não há uma maneira muito eficiente de fazer isso, e qualquer abordagem tem desvantagens significativas (por exemplo, verificar um intervalo de votação arrisca artefatos visuais e disputas entre questões de código). A verificação suja também requer um registro global de observadores, criando riscos de vazamento de memória e custos de eliminação que O.o() evita.

Vamos analisar alguns números.

Os testes comparativos abaixo (disponíveis no GitHub, em inglês) permitem comparar a verificação suja com o O.o(). Eles são estruturados como gráficos de Observed-Object-Set-Size vs Number-Of-Mutations. O resultado geral é que o desempenho da verificação incorreta é algoritmicamente proporcional ao número de objetos observados, enquanto o desempenho de O.o() é proporcional ao número de mutações feitas.

Verificação de itens

Desempenho da verificação incorreta

Chrome com Object.observe() ativado

Observar a performance

Preenchimento de Object.observe()

Ótimo! Então O.o() pode ser usado no Chrome 36, mas e em outros navegadores? Nós podemos ajudar. O Observe-JS do Polymer é um polyfill para O.o() que usa a implementação nativa se estiver presente. Caso contrário, o polyfill é aplicado e inclui algumas melhorias úteis. Ele oferece uma visão agregada do mundo que resume as mudanças e fornece um relatório do que mudou. Duas coisas realmente poderosas que ele expõe são:

  1. Você pode observar os caminhos. Isso significa que você pode dizer "Gostaria de observar "foo.bar.baz" em um determinado objeto e eles vão informar quando o valor do caminho mudou. Se o caminho está inacessível, ele considera o valor indefinido.

Exemplo de observação de um valor em um caminho de um determinado objeto:

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. Ele vai informar sobre mesclagens de matriz. As mesclagens de matriz são basicamente o conjunto mínimo de operações de união que você precisa executar em uma matriz para transformar a versão antiga na nova versão dela. Esse é um tipo de transformação ou visualização diferente da matriz. É a quantidade mínima de trabalho necessária para passar do estado antigo para o novo.

Exemplo de relatórios de mudanças em uma matriz como um conjunto mínimo de mesclagens:

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

Como mencionado, o O.o() oferecerá às estruturas e bibliotecas uma grande oportunidade de melhorar o desempenho da vinculação de dados em navegadores compatíveis com o recurso.

Yehuda Katz e Erik Bryn, da Ember, confirmaram que a inclusão do suporte para O.o() está no plano de curto prazo da Ember. Misko Hervy, do Angular, escreveu um documento de design sobre a detecção de mudanças melhorada do Angular 2.0. A abordagem de longo prazo será usar Object.observe() quando ele for lançado no Chrome estável, optando pelo Watchtower.js, a própria abordagem de detecção de mudanças até lá. Suuuuper emocionante.

Conclusões

O.o() é uma adição poderosa à plataforma da web que você pode usar hoje mesmo.

Esperamos que, com o tempo, o recurso seja lançado em mais navegadores, permitindo que as estruturas JavaScript obtenham aumentos de desempenho com o acesso a recursos de observação de objetos nativos. Os aplicativos destinados ao Chrome devem poder usar O.o() no Chrome 36 (e superior), e o recurso também deve estar disponível em uma versão futura do Opera.

Então, vá em frente e converse com os autores de frameworks JavaScript sobre o Object.observe() e como eles planejam usá-lo para melhorar o desempenho da vinculação de dados nos seus apps. Definitivamente, temos tempos emocionantes pela frente!

Recursos

Agradecemos a Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan e Vivian Cromwell pelas contribuições e avaliações.