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

Introdução

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

Sem mais delongas, Object.observe() foi lançada na versão estável do Chrome 36. [WOOOO. THE CROWD GOES WILD].

Object.observe(), parte de um futuro padrão ECMAScript, é um método para observar de forma assíncrona as mudanças em objetos JavaScript, sem a necessidade de uma biblioteca separada. Ele permite que um observador receba uma sequência ordenada no 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:

Mudança informada.

Com Object.observe() (gosto de chamar de O.o() ou Oooooooo), é possível 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 projetos grandes com lógica de negócios complicada, os frameworks opinativos são inestimáveis e você deve continuar usando. Eles simplificam a orientação de novos desenvolvedores, exigem menos manutenção de código e impõem padrões sobre como realizar tarefas comuns. Quando não precisar de uma, use bibliotecas menores e mais focadas, como a Polymer, que já aproveita o O.o().

Mesmo que você use muito uma framework ou biblioteca MV*, a O.o() pode oferecer algumas melhorias de desempenho saudáveis, com uma implementação mais rápida e simples, mantendo a mesma API. Por exemplo, no ano passado, o Angular descobriu que, em um comparativo de mercado em que mudanças estavam sendo feitas em um modelo, a verificação de estado sujo levava 40ms por atualização, e a O.o() levava de 1 a 2ms por atualização (uma melhoria de 20 a 40 vezes mais rápida).

A vinculação de dados sem a necessidade de toneladas de código complicado também significa que você não precisa mais verificar mudanças, o que prolonga a duração da bateria.

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

O que queremos observar?

Quando falamos sobre observação de dados, geralmente nos referimos a alguns tipos específicos de mudanças:

  • Mudanças em objetos JavaScript brutos
  • Quando as propriedades são adicionadas, alteradas ou excluídas
  • Quando as matrizes têm elementos divididos dentro e fora delas
  • Mudanças 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 de controle de modelo-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 cria alavancagem e economiza muito tempo na criação de código muito repetitivo que apenas envia dados de e para o DOM entre o estado interno do aplicativo ou o servidor.

A vinculação de dados é particularmente útil quando você tem uma interface do usuário complexa em que precisa conectar relacionamentos 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 dados de forma nativa no navegador, oferecemos aos frameworks JavaScript (e pequenas bibliotecas de utilitários que você escreve) uma maneira de observar mudanças nos dados do modelo sem depender de alguns dos hacks lentos que o mundo usa hoje.

Como o mundo é hoje

Verificação de estado

Onde você já viu a vinculação de dados? Se você usa uma biblioteca MV* moderna para criar seus aplicativos da Web (por exemplo, Angular, Knockout), provavelmente está acostumado a vincular dados de modelos ao DOM. Para relembrar, aqui está um exemplo de app de lista de telefones em que vinculamos o valor de cada telefone em uma matriz phones (definida em JavaScript) a um item de lista para que os 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 do 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 subjacente mudam, nossa lista no DOM é atualizada. Como o Angular consegue isso? Ele faz algo chamado "verificação de estado sujo" nos bastidores.

Verificação de dados sujos

A ideia básica da verificação de integridade é que, sempre que os dados podem ter mudado, a biblioteca precisa verificar se isso aconteceu por meio de um resumo ou ciclo de mudança. No caso do Angular, um ciclo de resumo identifica todas as expressões registradas para serem observadas e verificar se há uma mudança. Ele conhece os valores anteriores de um modelo e, se eles tiverem mudado, um evento de mudança será acionado. Para um desenvolvedor, o principal benefício é que você pode usar dados de objetos JavaScript brutos, que são agradáveis de usar e se compõe muito bem. A desvantagem é que ele tem um comportamento algorítmico ruim e pode ser muito caro.

Verificação suja.

A despesa dessa operação é proporcional ao número total de objetos observados. Talvez seja necessário fazer muitas verificações sujas. Também pode ser necessário acionar a verificação de integridade quando os dados possam ter mudado. Há muitos truques inteligentes que os frameworks usam para isso. Não está claro se isso vai ser perfeito.

O ecossistema da Web precisa ter mais capacidade de inovar e evoluir os próprios mecanismos declarativos, por exemplo:

  • Sistemas de modelos baseados em restrições
  • Sistemas de autopersistência (por exemplo, mudanças persistentes no IndexedDB ou localStorage)
  • Objetos de contêiner (Ember, Backbone)

Os objetos container são onde um framework cria objetos que armazenam os dados. Eles têm acessórios aos dados e podem capturar o que você define ou recebe e transmitir internamente. Isso funciona bem. Ele tem um bom desempenho e um bom comportamento algorítmico. Confira abaixo um exemplo de objetos de contêiner usando o 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

O custo de descobrir o que mudou aqui é proporcional ao número de coisas que mudaram. Outro problema é que você está usando um tipo diferente de objeto. Em geral, é necessário converter os dados recebidos do servidor para esses objetos para que eles sejam observáveis.

Isso não funciona muito bem com o código JS atual, porque a maioria dos códigos presume que pode operar com dados brutos. Não para esses tipos especializados de objetos.

Introducing Object.observe()

O ideal é ter o melhor dos dois mundos: uma maneira de observar dados com suporte a objetos de dados brutos (objetos JavaScript normais) se quisermos E sem precisar verificar tudo o tempo todo. Algo com um bom comportamento algorítmico. Algo que se integra bem e é incorporado à plataforma. Essa é a beleza do que Object.observe() traz para a mesa.

Ele permite observar um objeto, modificar propriedades e conferir o relatório de mudanças. Mas chega de teoria. Vamos analisar o código.

Object.observe()

Object.observe() e Object.unobserve()

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

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

Podemos especificar um callback sempre que ocorrerem mutações (mudanças) 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 do modelo Todos:

todoModel.label = 'Buy some more milk';

Ao analisar o console, recebemos algumas informações úteis. Sabemos qual propriedade mudou, como ela foi alterada e qual é o novo valor.

Relatório do console

Uhu! Adeus, verificação de sujeira! Sua lápide deve ser gravada em Comic Sans. Vamos mudar outra propriedade. Desta vez, completeBy:

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

Como podemos ver, recebemos novamente um relatório de mudança:

Mude o relatório.

Ótimo. E se você decidir excluir a propriedade "concluída" do objeto?

delete todoModel.completed;
Concluído

Como podemos ver, o relatório de mudanças retornadas inclui informações sobre a exclusão. Como esperado, o novo valor da propriedade agora está indefinido. Agora você sabe que é possível descobrir quando as propriedades foram adicionadas. Quando forem excluídos. Basicamente, o conjunto de propriedades em um objeto ("novo", "excluído", "reconfigurado") e a mudança de protótipo (proto).

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

Object.unobserve(todoModel, observer);

Como podemos ver abaixo, qualquer mutação feita no objeto depois que ele foi executado não resulta mais em uma lista de registros de mudança retornados.

Mutações

Como especificar mudanças de interesse

Analisamos os conceitos básicos de como recuperar uma lista de mudanças em um objeto observado. E se você quiser apenas um subconjunto de mudanças feitas em um objeto, em vez de todas elas? Todo mundo precisa de um filtro de spam. Os observadores podem especificar apenas os tipos de mudanças que querem receber em uma lista de aceitação. Isso pode ser especificado usando o terceiro argumento para O.o() da seguinte maneira:

Object.observe(obj, callback, optAcceptList)

Confira 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 excluirmos o rótulo, esse tipo de mudança será informado:

delete todoModel.label;

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

Notificações

O O.o() também tem a noção de notificações. Eles não são nada como aquelas coisas irritantes que você recebe em um smartphone, mas são bastante úteis. As notificações são semelhantes aos observadores de mutação. Eles acontecem no final da microtarefa. No contexto do navegador, isso 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 deles. É um bom modelo de processamento por turnos.

O fluxo de trabalho para usar um notificador é mais ou menos assim:

Notificações

Vamos conferir um exemplo de como os notificadores podem ser usados na prática para definir notificações personalizadas para quando as propriedades de um objeto são acessadas ou definidas. Confira os 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 muda ("update"). Qualquer outra coisa que a implementação do objeto escolha informar (notifier.notifyChange()).

Anos de experiência na plataforma da Web nos ensinaram que uma abordagem síncrona é a primeira coisa que você tenta porque é a mais fácil de entender. O problema é que ele cria um modelo de processamento fundamentalmente perigoso. Se você estiver escrevendo código e, por exemplo, atualizar a propriedade de um objeto, não vai querer que a atualização da propriedade desse objeto possa ter convidado algum código arbitrário para fazer o que quiser. Não é ideal invalidar suas suposições enquanto você está no meio de uma função.

Se você for um observador, não será chamado se alguém estiver no meio de algo. Você não quer que peçam para trabalhar em um estado inconsistente do mundo. Fazer mais verificações de erros. Tentar tolerar mais situações ruins e, em geral, é um modelo difícil de trabalhar. O modo assíncrono é mais difícil de lidar, mas é um modelo melhor no final do dia.

A solução para esse problema são registros de mudança sintéticos.

Registros de mudança sintéticos

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

Registros de mudança sintéticos

A observação de acionadores 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.o não faz julgamentos sobre a maneira "certa". As propriedades computadas precisam ser acessórios que notificam quando o estado interno (privado) muda.

Novamente, os desenvolvedores da Web devem esperar que as bibliotecas ajudem a facilitar a notificação e várias abordagens de propriedades computadas (e reduzam o boilerplate).

Vamos configurar 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 acessório e, quando o valor dele muda, ele notifica a si mesmo que o valor mudou. Isso será enviado com todas as outras mudanças feitas nesse objeto ou em qualquer outro. Basicamente, se você está implementando um objeto que quer ter propriedades sintéticas ou computadas, ou precisa escolher uma estratégia para que isso funcione. Depois disso, ele vai se encaixar no seu sistema como um todo.

Pule o código para conferir como ele funciona 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 mudanças sintéticas

Propriedades de acessório

Uma observação rápida sobre as propriedades de acessório. Mencionamos anteriormente que apenas as mudanças de valor são observáveis para propriedades de dados. Não para propriedades computadas ou acessórios. Isso ocorre porque o JavaScript não tem a noção de mudanças de valor para os acionadores. Um acessório é apenas um conjunto de funções.

Se você atribuir a um acionador, o JavaScript vai invocar a função e, do ponto de vista dele, nada mudou. Ele apenas deu a oportunidade de executar algum código.

O problema é que, semanticamente, podemos analisar a atribuição acima ao valor - 5. Devemos saber o que aconteceu aqui. Esse é um problema sem solução. O exemplo demonstra o motivo. Não há como qualquer sistema saber o que isso significa, porque pode ser um código arbitrário. Ele pode fazer o que quiser nesse caso. O valor é atualizado sempre que é acessado. Portanto, perguntar se ele mudou não faz muito sentido.

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 vai receber o conjunto completo de mudanças de todos os objetos que ele observa no "fim da microtarefa" (observe a semelhança com os observadores de mutação).

Observar vários objetos com um callback

Mudanças em grande escala

Talvez você esteja trabalhando em um app muito grande e precise trabalhar regularmente com mudanças em grande escala. Os objetos podem querer descrever mudanças semânticas maiores que afetam muitas propriedades de maneira mais compacta (em vez de transmitir várias mudanças de propriedade).

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

Mudanças em grande escala

Vamos analisar isso em um exemplo de como as mudanças em grande escala podem ser descritas, em que definimos um objeto Thingy com alguns utilitários matemáticos (multiplicar, incrementar, incrementarEmultiplicar). Sempre que um utilitário é usado, ele informa ao sistema que um conjunto de trabalho inclui um tipo específico de mudança.

Por 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 é um coletor de mudanças e outro que só informa 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 brincar com esse código. Vamos definir um novo Thingy:

var thingy = new Thingy(2, 4);

Observe e faça algumas mudanças. OMG, muito divertido. Tantas 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 dentro da "função de execução" é considerado o trabalho de "grande mudança". Os observadores que aceitam "grande mudança" só recebem o registro de "grande mudança". Os observadores que não receberem as mudanças subjacentes resultantes do trabalho que "executar função" receberam.

Como observar matrizes

Já falamos sobre observar mudanças em objetos, mas e as matrizes? Ótima pergunta. Quando alguém me diz: "Ótima pergunta". Nunca ouço a resposta porque estou ocupado me parabenizando por fazer uma pergunta tão boa, mas estou divagando. Também temos novos métodos para trabalhar com matrizes.

Array.observe() é um método que trata mudanças em grande escala, como splice, unshift ou qualquer coisa que mude implicitamente o comprimento, como um registro de mudança "splice". Internamente, ele usa notifier.performChange("splice",...).

Confira um exemplo em que observamos uma "matriz" de modelo e, da mesma forma, recebemos uma lista de mudanças quando há alterações 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

O impacto da performance computacional de O.o() é como um cache de leitura. De modo geral, um cache é uma ótima escolha quando (em ordem de importância):

  1. A frequência de leituras domina a frequência de gravações.
  2. É possível criar um cache que troca a quantidade constante de trabalho envolvido nas gravações por um desempenho mais eficiente do algoritmo durante as leituras.
  3. A desaceleração constante das gravações é aceitável.

O método O.o() foi criado para casos de uso como 1).

A verificação de dados sujos exige manter uma cópia de todos os dados que você está observando. Isso significa que você incorre em um custo de memória estrutural para a verificação de integridade que não é possível com O.o(). A verificação de integridade, embora seja uma solução temporária decente, também é uma abstração fundamentalmente inadequada, que pode criar complexidade desnecessária para os aplicativos.

Por quê? A verificação de integridade precisa ser executada sempre que os dados possam ter mudado. Não há uma maneira muito robusta de fazer isso, e qualquer abordagem tem desvantagens significativas (por exemplo, a verificação em um intervalo de pesquisa gera artefatos visuais e condições de corrida entre problemas de código). A verificação de estado sujo também exige um registro global de observadores, criando riscos de vazamento de memória e custos de desmontagem que O.o() evita.

Vamos conferir alguns números.

Os testes de comparação abaixo (disponíveis no GitHub) permitem comparar a verificação de estado sujo com a O.o(). Eles são estruturados como gráficos de tamanho de conjunto de objetos observados x número de mutações. O resultado geral é que a performance da verificação de alteração é proporcional ao número de objetos observados, enquanto a performance de O.o() é proporcional ao número de mutações feitas.

Verificação de estado

Desempenho da verificação de sujeira

Chrome com Object.observe() ativado

Observar a performance

Preenchimento polido de Object.observe()

Ótimo, a O.o() pode ser usada no Chrome 36, mas e em outros navegadores? Nós podemos ajudar. O Observe-JS do Polymer é um polyfill para O.o(), que vai usar a implementação nativa se ela estiver presente, mas, caso contrário, vai polyfillá-la e incluir algumas funcionalidades úteis. Ele oferece uma visão agregada do mundo que resume as mudanças e gera um relatório do que mudou. Duas coisas muito importantes que ele expõe são:

  1. Você pode observar caminhos. Isso significa que você pode dizer "Gostaria de observar "foo.bar.baz" em um determinado objeto, e ele vai informar quando o valor nesse caminho mudar. Se o caminho não puder ser acessado, o valor será considerado 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 as emendas de matriz. As emendas de matriz são basicamente o conjunto mínimo de operações de emenda que você precisa realizar em uma matriz para transformar a versão antiga da matriz na nova. Esse é um tipo de transformação ou visualização diferente da matriz. É o trabalho mínimo que você precisa fazer para passar do estado antigo para o novo.

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

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, a O.o() vai dar aos frameworks e bibliotecas uma grande oportunidade de melhorar a performance da vinculação de dados em navegadores compatíveis com o recurso.

Yehuda Katz e Erik Bryn, da Ember, confirmaram que a adição de suporte para O.o() está no roteiro de curto prazo da Ember. Misko Hervy, do Angular, escreveu um documento de design sobre a detecção de mudanças aprimorada do Angular 2.0. A abordagem de longo prazo será aproveitar o Object.observe() quando ele for lançado na versão estável do Chrome, optando por Watchtower.js, a própria abordagem de detecção de mudanças até então. Que demais.

Conclusões

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

Esperamos que o recurso seja lançado em mais navegadores, permitindo que os frameworks JavaScript tenham um aumento de desempenho com o acesso a recursos de observação de objetos nativos. Os desenvolvedores que segmentam o Chrome poderão usar O.o() no Chrome 36 (e versões mais recentes), e o recurso também estará disponível em uma versão futura do Opera.

Então, converse com os autores de frameworks JavaScript sobre Object.observe() e como eles planejam usá-lo para melhorar a performance da vinculação de dados nos seus apps. Há muitas coisas incríveis por vir.

Recursos

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