Революция в связывании данных с помощью Object.observe()

Адди Османи
Addy Osmani

Введение

Грядет революция. В JavaScript появилось новое дополнение, которое изменит все, что вы знаете о привязке данных. Также изменится подход многих ваших библиотек MVC к наблюдению за изменениями и обновлениями моделей. Готовы ли вы к приятному повышению производительности приложений, которые заботятся о наблюдении за недвижимостью?

Хорошо. Хорошо. Без промедления я рад сообщить, Object.observe() появился в стабильной версии Chrome 36 . [УУУУ. ТОЛПА СХОДИТ.] .

Object.observe() , часть будущего стандарта ECMAScript, представляет собой метод асинхронного наблюдения за изменениями в объектах JavaScript… без необходимости использования отдельной библиотеки. Это позволяет наблюдателю получать упорядоченную по времени последовательность записей изменений, которые описывают набор изменений, произошедших с набором наблюдаемых объектов.

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

});

При каждом внесении изменения об этом сообщается:

Сообщено об изменении.

С помощью Object.observe() (мне нравится называть его Oo() или Oooooooo) вы можете реализовать двустороннюю привязку данных без необходимости использования фреймворка .

Это не значит, что вы не должны его использовать. Для крупных проектов со сложной бизнес-логикой самоуверенные фреймворки неоценимы, и вам следует продолжать их использовать. Они упрощают ориентацию новых разработчиков, требуют меньше обслуживания кода и навязывают шаблоны для решения общих задач. Если они вам не нужны, вы можете использовать более мелкие и более специализированные библиотеки, такие как Polymer (которые уже используют преимущества Oo()).

Даже если вы интенсивно используете фреймворк или библиотеку MV*, Oo() может обеспечить им значительное улучшение производительности, более быструю и простую реализацию при сохранении того же API. Например, в прошлом году Angular обнаружил , что в тесте, где в модель вносились изменения, грязная проверка занимала 40 мс на каждое обновление, а Oo() — 1–2 мс на каждое обновление (улучшение в 20–40 раз быстрее).

Привязка данных без необходимости написания тонны сложного кода также означает, что вам больше не придется запрашивать изменения, что увеличивает срок службы батареи!

Если вы уже заинтересовались Oo(), переходите к описанию функции или читайте дальше, чтобы узнать больше о проблемах, которые она решает.

Что мы хотим наблюдать?

Когда мы говорим о наблюдении за данными, мы обычно имеем в виду отслеживание некоторых конкретных типов изменений:

  • Изменения в необработанных объектах JavaScript
  • Когда свойства добавляются, изменяются, удаляются
  • Когда в массивах есть элементы, входящие и выходящие из них
  • Изменения в прототипе объекта

Важность привязки данных

Привязка данных становится важной, когда вы заботитесь о разделении элементов управления моделью и представлением. HTML — отличный декларативный механизм, но он полностью статичен. В идеале вы просто хотите объявить связь между вашими данными и DOM и поддерживать DOM в актуальном состоянии. Это создает преимущества и экономит вам много времени на написание действительно повторяющегося кода, который просто отправляет данные в DOM и обратно между внутренним состоянием вашего приложения или сервером.

Привязка данных особенно полезна, когда у вас сложный пользовательский интерфейс, в котором вам необходимо связать отношения между несколькими свойствами в ваших моделях данных с несколькими элементами в ваших представлениях. Это довольно часто встречается в одностраничных приложениях, которые мы создаем сегодня.

Создавая способ естественного наблюдения за данными в браузере, мы даем фреймворкам JavaScript (и небольшим служебным библиотекам, которые вы пишете) возможность наблюдать за изменениями в данных модели, не полагаясь на некоторые медленные хаки, которые мир использует сегодня.

Как выглядит мир сегодня

Грязная проверка

Где вы раньше видели привязку данных? Что ж, если вы используете современную библиотеку MV* для создания своих веб-приложений (например, Angular, Knockout), вы, вероятно, привыкли привязывать данные модели к DOM. В качестве напоминания, вот пример приложения «Список телефонов», в котором мы привязываем значение каждого телефона в phones (определенном в JavaScript) к элементу списка, чтобы наши данные и пользовательский интерфейс всегда синхронизировались:

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

и JavaScript для контроллера:

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

Каждый раз, когда данные базовой модели изменяются, наш список в DOM обновляется. Как Angular достигает этого? Ну, за кулисами он делает так называемую грязную проверку.

Грязная проверка

Основная идея грязной проверки заключается в том, что каждый раз, когда данные могут измениться, библиотека должна проверить, изменились ли они, с помощью дайджеста или цикла изменений. В случае Angular цикл дайджеста идентифицирует все выражения, зарегистрированные для просмотра, чтобы увидеть, есть ли изменения. Он знает о предыдущих значениях модели, и если они изменились, генерируется событие изменения. Для разработчика основным преимуществом здесь является то, что вы можете использовать необработанные данные объекта JavaScript, которые приятно использовать и которые довольно хорошо компонуются. Обратной стороной является то, что он имеет плохое алгоритмическое поведение и потенциально очень дорог.

Грязная проверка.

Стоимость этой операции пропорциональна общему количеству наблюдаемых объектов. Возможно, мне придется провести много грязных проверок. Также может потребоваться способ запускать грязную проверку, когда данные могли измениться. Для этого существует множество хитроумных трюков. Неясно, будет ли это когда-нибудь идеально.

Веб-экосистема должна иметь больше возможностей для инноваций и развития своих собственных декларативных механизмов, например

  • Системы моделей на основе ограничений
  • Системы автоматического сохранения (например, сохранение изменений в IndexedDB или localStorage)
  • Объекты-контейнеры (Ember, Backbone)

Объекты -контейнеры — это место, где платформа создает объекты, внутри которых хранятся данные. У них есть доступ к данным, и они могут фиксировать то, что вы устанавливаете или получаете, и транслировать их внутри компании. Это работает хорошо. Он относительно производительен и имеет хорошее алгоритмическое поведение. Пример объектов-контейнеров с использованием 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

Затраты на обнаружение того, что здесь изменилось, пропорциональны количеству вещей, которые изменились. Другая проблема заключается в том, что теперь вы используете объект другого типа. Вообще говоря, вам необходимо преобразовать данные, которые вы получаете с сервера, в эти объекты, чтобы их можно было наблюдать.

Это не особенно хорошо сочетается с существующим кодом JS, поскольку большая часть кода предполагает, что он может работать с необработанными данными. Не для этих специализированных объектов.

Introducing Object.observe()

В идеале нам нужно лучшее из обоих миров — способ наблюдения за данными с поддержкой необработанных объектов данных (обычных объектов JavaScript), если мы решим И без необходимости постоянно все проверять. Что-то с хорошим алгоритмическим поведением. Что-то, что хорошо компонуется и встроено в платформу. В этом красота того, что предлагает Object.observe() .

Это позволяет нам наблюдать за объектом, изменять свойства и видеть отчет об изменениях. Но хватит теории, давайте посмотрим на код!

Объект.наблюдать()

Object.observe() и Object.unobserve()

Давайте представим, что у нас есть простой объект JavaScript, представляющий модель:

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

Затем мы можем указать обратный вызов всякий раз, когда в объект вносятся мутации (изменения):

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

Затем мы можем наблюдать за этими изменениями с помощью Oo(), передавая объект в качестве первого аргумента, а обратный вызов в качестве второго:

Object.observe(todoModel, observer);

Давайте начнем вносить некоторые изменения в наш объект модели Todos:

todoModel.label = 'Buy some more milk';

Заглянув в консоль, мы получаем полезную информацию! Мы знаем, какое свойство изменилось, как оно было изменено и каково новое значение.

Консольный отчет

Ву! Прощай, грязная проверка! Ваше надгробие должно быть вырезано в Comic Sans. Давайте изменим еще одно свойство. На этот раз completeBy :

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

Как мы видим, мы снова успешно получили отчет об изменениях:

Изменить отчет.

Большой. Что, если мы теперь решим удалить свойство «завершено» из нашего объекта:

delete todoModel.completed;
Завершенный

Как мы видим, возвращенный отчет об изменениях включает информацию об удалении. Как и ожидалось, новое значение свойства теперь не определено. Итак, теперь мы знаем, что вы можете узнать, когда были добавлены свойства. Когда они были удалены. По сути, это набор свойств объекта («новый», «удаленный», «перенастроенный») и изменение его прототипа ( proto ).

Как и в любой системе наблюдения, существует метод, позволяющий перестать прислушиваться к изменениям. В данном случае это Object.unobserve() , который имеет ту же сигнатуру, что и Oo(), но может вызываться следующим образом:

Object.unobserve(todoModel, observer);

Как мы видим ниже, любые мутации, внесенные в объект после его запуска, больше не приводят к возврату списка записей изменений.

Мутации

Указание интересующих изменений

Итак, мы рассмотрели основы получения списка изменений наблюдаемого объекта. Что, если вас интересует только часть изменений, внесенных в объект, а не все из них? Каждому нужен спам-фильтр. Что ж, наблюдатели могут указать только те типы изменений, о которых они хотят услышать, через список принятия. Это можно указать с помощью третьего аргумента функции Oo() следующим образом:

Object.observe(obj, callback, optAcceptList)

Давайте рассмотрим пример того, как это можно использовать:

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

Однако если мы теперь удалим метку, обратите внимание, что об этом типе изменения действительно сообщается:

delete todoModel.label;

Если вы не укажете список типов принятия для Oo(), по умолчанию будут использоваться «внутренние» типы изменения объекта ( add , update , delete , reconfigure , preventExtensions (когда объект, становящийся нерасширяемым, не наблюдается). ).

Уведомления

Oo() также включает в себя понятие уведомлений. Они не похожи на те раздражающие вещи, которые есть в телефоне, но весьма полезны. Уведомления аналогичны Mutation Observers . Они происходят в конце микрозадачи. В контексте браузера это почти всегда будет в конце текущего обработчика событий.

Время выбрано удачное, потому что обычно одна единица работы завершена, и теперь наблюдатели могут приступить к своей работе. Это хорошая пошаговая модель обработки.

Рабочий процесс использования уведомления выглядит примерно так:

Уведомления

Давайте рассмотрим пример того, как уведомители могут использоваться на практике для определения пользовательских уведомлений при получении или установке свойств объекта. Следите за комментариями здесь:

// 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);
Консоль уведомлений

Здесь мы сообщаем, когда значение свойств данных изменяется («обновляется»). Все остальное, что реализация объекта решит сообщить ( notifier.notifyChange() ).

Многолетний опыт работы с веб-платформой научил нас, что синхронный подход — это первое, что вы пробуете, потому что его проще всего усвоить. Проблема в том, что это создает фундаментально опасную модель обработки. Если вы пишете код и говорите: «Обновите свойство объекта», вы на самом деле не хотите, чтобы ситуация, в которой обновление свойства этого объекта могла привести к тому, что какой-то произвольный код сделал все, что он хотел. Не идеально, если ваши предположения будут признаны недействительными, когда вы выполняете середину функции.

Если вы наблюдатель, в идеале вы не хотите, чтобы вам звонили, если кто-то что-то делает. Вы не хотите, чтобы вас попросили поработать над противоречивым состоянием мира. В конечном итоге придется делать гораздо больше проверок на ошибки. Пытаться терпеть гораздо больше плохих ситуаций и, как правило, с этой моделью сложно работать. С Async сложнее иметь дело, но, в конце концов, это лучшая модель.

Решением этой проблемы являются синтетические записи изменений.

Синтетические записи изменений

По сути, если вы хотите иметь средства доступа или вычисляемые свойства, вы обязаны уведомлять об изменении этих значений. Это небольшая дополнительная работа, но она задумана как своего рода первоклассная функция этого механизма, и эти уведомления будут доставляться вместе с остальными уведомлениями от базовых объектов данных. Из свойств данных.

Синтетические записи изменений

Наблюдение за аксессорами и вычисляемыми свойствами можно решить с помощью notifier.notify — другой части Oo(). Большинству систем наблюдения нужна та или иная форма наблюдения производных значений. Есть много способов сделать это. Оо не делает суждений о «правильном» пути. Вычисляемые свойства должны быть средствами доступа, которые уведомляют об изменении внутреннего (частного) состояния.

Опять же, веб-разработчикам следует ожидать, что библиотеки помогут упростить уведомления и различные подходы к вычисляемым свойствам (и сократить количество шаблонов).

Давайте создадим следующий пример — класс круга. Идея здесь в том, что у нас есть этот круг и есть свойство радиуса. В этом случае радиус является аксессором, и когда его значение изменится, он фактически уведомит себя об изменении значения. Это будет доставлено со всеми другими изменениями в этом объекте или любом другом объекте. По сути, если вы реализуете объект, вы хотите иметь синтетические или вычисляемые свойства или вам нужно выбрать стратегию того, как это будет работать. Как только вы это сделаете, это впишется в вашу систему в целом.

Пропустите код, чтобы увидеть, как это работает в 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);
  })
}
Консоль синтетических записей изменений

Свойства аксессора

Небольшое примечание о свойствах аксессора. Ранее мы упоминали, что для свойств данных наблюдаются только изменения значений. Не для вычисляемых свойств или средств доступа. Причина в том, что в JavaScript на самом деле нет понятия изменения значений аксессоров. Аксессор — это просто набор функций.

Если вы назначаете метод доступа, JavaScript просто вызывает функцию, и с его точки зрения ничего не меняется. Это просто дало возможность запуститься некоторому коду.

Проблема в том, что семантически мы можем посмотреть на приведенное выше присвоение значения — 5. Мы должны знать, что здесь произошло. На самом деле это неразрешимая проблема. Пример показывает, почему. На самом деле ни одна система не может узнать, что под этим подразумевается, поскольку это может быть произвольный код. В этом случае он может делать все, что захочет. Он обновляет значение каждый раз, когда к нему обращаются, поэтому спрашивать, изменилось ли оно, не имеет особого смысла.

Наблюдение за несколькими объектами с помощью одного обратного вызова

Другой шаблон, возможный с помощью Oo(), — это понятие одного наблюдателя обратного вызова. Это позволяет использовать один обратный вызов в качестве «наблюдателя» для множества различных объектов. Обратный вызов будет доставлять полный набор изменений всем объектам, которые он наблюдает, в «конце микрозадачи» (обратите внимание на сходство с наблюдателями мутаций).

Наблюдение за несколькими объектами с помощью одного обратного вызова

Масштабные изменения

Возможно, вы работаете над ооочень большим приложением и вам регулярно приходится вносить масштабные изменения. Объекты могут захотеть описывать более крупные семантические изменения, которые будут влиять на множество свойств более компактным способом (вместо того, чтобы транслировать тонны изменений свойств).

Oo() помогает в этом в виде двух конкретных утилит: notifier.performChange() и notifier.notify() , о которых мы уже рассказывали.

Масштабные изменения

Давайте посмотрим на это на примере того, как можно описать крупномасштабные изменения, когда мы определяем объект Thingy с помощью некоторых математических утилит (multiply, Increment, IncrementAndMultiply). Каждый раз, когда используется утилита, она сообщает системе, что совокупность работ включает в себя изменения определенного типа.

Например: 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
    });
  }
}

Затем мы определяем для нашего объекта двух наблюдателей: один, который будет отслеживать все изменения, и другой, который будет сообщать только об определенных нами типах принятия (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);
}

Теперь мы можем начать играть с этим кодом. Давайте определим новую вещь:

var thingy = new Thingy(2, 4);

Понаблюдайте за этим, а затем внесите некоторые изменения. О боже, так весело. ТАК много штучек!

// 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 }
Масштабные изменения

Все, что находится внутри «функции исполнения», считается работой «больших перемен». Наблюдатели, которые принимают «большие изменения», получат только запись «больших изменений». Наблюдатели, которые этого не делают, получат основные изменения, возникшие в результате работы, которую проделала «выполняющая функция».

Наблюдение за массивами

Мы уже говорили о наблюдении за изменениями объектов, но как насчет массивов?! Отличный вопрос. Когда кто-то говорит мне: «Отличный вопрос». Я никогда не слышу их ответа, потому что занят поздравлениями с тем, что задал такой замечательный вопрос, но я отвлекся. У нас также есть новые методы работы с массивами!

Array.observe() — это метод, который обрабатывает крупномасштабные изменения самого себя (например, сращивание, смещение или что-либо еще, что неявно меняет длину) как запись изменения «сращивания». Внутри он использует notifier.performChange("splice",...) .

Вот пример, в котором мы наблюдаем «массив» модели и аналогичным образом получаем список изменений при наличии каких-либо изменений в базовых данных:

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';
Наблюдение за массивами

Производительность

О влиянии Oo() на вычислительную производительность можно думать как о кэше чтения. Вообще говоря, кэш — отличный выбор, когда (в порядке важности):

  1. Частота чтения доминирует над частотой записи.
  2. Вы можете создать кеш, который заменяет постоянный объем работы, выполняемой при записи, алгоритмически более высокой производительностью при чтении.
  3. Постоянное замедление записи допустимо.

Oo() предназначен для таких случаев использования, как 1).

Грязная проверка требует сохранения копий всех данных, которые вы наблюдаете. Это означает, что вы несете затраты структурной памяти на грязную проверку, которую вы просто не можете получить с помощью Oo(). Грязная проверка, хотя и является достойным временным решением, в то же время является фундаментально дырявой абстракцией, которая может создать ненужную сложность для приложений.

Почему? Ну, грязную проверку необходимо запускать каждый раз, когда данные могут измениться. Просто не существует очень надежного способа сделать это, и любой подход к нему имеет существенные недостатки (например, проверка интервала опроса может привести к появлению визуальных артефактов и условий гонки между проблемами кода). Для грязной проверки также требуется глобальный реестр наблюдателей, что создает опасность утечки памяти и затраты на демонтаж, которых Oo() позволяет избежать.

Давайте посмотрим на некоторые цифры.

Приведенные ниже тесты производительности (доступны на GitHub ) позволяют нам сравнить грязную проверку с Oo(). Они структурированы в виде графиков зависимости размера набора наблюдаемых объектов от числа мутаций. Общий результат таков, что производительность грязной проверки алгоритмически пропорциональна количеству наблюдаемых объектов, тогда как производительность Oo() пропорциональна количеству сделанных мутаций.

Грязная проверка

Грязная проверка производительности

Chrome с включенным Object.observe()

Наблюдайте за производительностью

Полизаполнение Object.observe()

Отлично, значит, Oo() можно использовать в Chrome 36, но как насчет использования его в других браузерах? Мы вас прикроем. Observe-JS от Polymer — это полифилл для Oo(), который будет использовать нативную реализацию, если она присутствует, но в противном случае заполняет ее и включает некоторые полезные шугаринги сверху. Он предлагает совокупное представление о мире, которое суммирует изменения и предоставляет отчет о том, что изменилось. Две действительно важные вещи, которые он раскрывает:

  1. Вы можете наблюдать пути. Это означает, что вы можете сказать: «Я хотел бы наблюдать за «foo.bar.baz» из данного объекта, и они сообщат вам, когда значение по этому пути изменится. Если путь недостижим, он считает значение неопределенным.

Пример наблюдения значения на пути от данного объекта:

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. Он расскажет вам о сращивании массивов. Сращивание массивов — это, по сути, минимальный набор операций сращивания, которые вам придется выполнить над массивом, чтобы преобразовать старую версию массива в новую версию массива. Это тип преобразования или другой вид массива. Это минимальный объем работы, который необходимо выполнить, чтобы перейти из старого состояния в новое.

Пример сообщения об изменениях в массиве как минимальном наборе склеек:

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

Фреймворки и Object.observe()

Как уже упоминалось, Oo() предоставит платформам и библиотекам огромную возможность улучшить производительность привязки данных в браузерах, поддерживающих эту функцию.

Иегуда Кац и Эрик Брин из Ember подтвердили, что добавление поддержки Oo() входит в планы Ember на ближайшую перспективу. Миско Херви из Angular написал проектную документацию по улучшенному обнаружению изменений в Angular 2.0. Их долгосрочный подход будет заключаться в том, чтобы воспользоваться преимуществами Object.observe(), когда он появится в стабильной версии Chrome, выбрав Watchtower.js , их собственный подход к обнаружению изменений до тех пор. Оооочень захватывающе.

Выводы

Oo() — это мощное дополнение к веб-платформе, которое вы можете использовать уже сегодня.

Мы надеемся, что со временем эта функция появится в большем количестве браузеров, что позволит платформам JavaScript повысить производительность за счет доступа к собственным возможностям наблюдения за объектами. Те, кто ориентируется на Chrome, должны иметь возможность использовать Oo() в Chrome 36 (и более поздних версиях), и эта функция также должна быть доступна в будущей версии Opera.

Итак, поговорите с авторами фреймворков JavaScript об Object.observe() и о том, как они планируют использовать его для повышения производительности привязки данных в ваших приложениях. Впереди определенно захватывающие времена!

Ресурсы

Выражаем благодарность Рафаэлю Вайнштейну, Джейку Арчибальду, Эрику Бидельману, Полу Кинлану и Вивиан Кромвель за их вклад и отзывы.