Революция дата-байндинга с Object.Observe()

http://www.html5rocks.com/en/tutorials/es7/observe/
  • Перевод

Введение


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

Хорошо, хорошо. Не будем тянуть. Я рад вам представить Object.observe(), который появился в бета версии Chrome 36. [ТОЛПА ЛИКУЕТ]

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


// Давайте представим, что у нас есть модель с данными
var model = {};

// Изменения которой мы начинаем отслеживать
Object.observe(model, function(changes){

    // Эта асинхронная возвращаемая функция запускается,
    changes.forEach(function(change) {

        // Давая нам понять, что изменилось
        console.log(change.type, change.name, change.oldValue);
    });

});

При каждом изменении объекта мы получаем оповещение:


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

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

Даже если вы во всю используете фреймворки или MV* библиотеки, O.o() позволит им получить неплохой прирост в производительности, который достигается за счёт быстрой, упрощенной реализации, и в то же время продолжает использовать тот же интерфейс API. Например, последний год команда разработчиков Angular провела сравнительный анализ и установила, что dirty-checking занимает приблизительно 40мс, в то время как О.о() занимает где-то 1-2мс (получается, быстрее в 20-40 раз).
Дата-байндинг без необходимости использовать тонны сложного кода! А ведь это так же означает, что вам больше не придется опрашивать модель на получение изменений!

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

За чем мы собираемся наблюдать?


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

О важности дата-байндинга


Дата-байндинг начинает становится важной частью вашего приложения, когда вы начинаете затрагивать взаимодействие модели и представления. HTML является великолепным декларативным механизмом, но он полностью статичен. В идеале, вы просто хотите связать ваши данные с DOM и держать его в постоянно актуальном состоянии. Решение с O.o() позволяет вам сэкономить большое количество времени, за счёт отсутствия необходимости писать большие куски повторяющегося кода, который будет просто посылать новые данные в DOM, и наобарот.

Дата-байндинг действительно удобен, когда вы создаете комплексный пользовательский интерфейс, где вам необходимо наладить большое количество связей между различными свойствами ваших моделей и UI элементами, отражающими их. Это одна из самых распространенных задач во время создания SPA (Single Page Application) приложений.

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

Что мир представляет из себя сегодня?


Dirty-checking

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

<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">
        {{phone.name}}
        <p>{{phone.snippet}}</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 достигает этого? Что ж, за сценой он выполняет то, что мы называется dirty-checking.



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



Расходы на такие операции пропорциональны общему количеству наблюдаемых объектов. Возможно, мне потребуется делать очень много подобных проверок. Так же возможно, что мне потребуется способ вызвать события dirty-checking´а, когда модель «возможно» изменилась. Существует довольно много хитрых трюков, к которым прибегает фреймворк, чтобы реализовать подобные решения. Пока еще непонятно, доведут ли данное решение до ума.

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

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

// Объект-контейнер
MyApp.president = Ember.Object.create({
  name: "Barak Obama"
});
 
MyApp.country = Ember.Object.create({
  // суффикс "Binding" в названии свойства говорит Ember о том,
  // что необходимо создать байндинг к этому свойству
  presidentNameBinding: "MyApp.president.name"
});
 
// Позже, после того как Ember создал байндинг
MyApp.country.get("presidentName");
// "Барак Обама"
 
// Данные с сервера должны быть конвертированы,
// т.к. они не соответствуют существующему коду.

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

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

Введение в Object.observe()


Было бы по-настоящему здорово, если бы мы могли получить лучшее от этих двух вселенных: возьмем возможность наблюдать за изменением данных с поддержкой для обычных объектов (нативные объекты JavaScript) и уберем dirty-checking, а вместо него добавим какой-нибудь алгоритм с хорошим характеристиками. Какой-нибудь такой, который будет объединять все эти положительные качества и будет встроен в платформу. Так вот, встречайте — Object.observe(), уже готовый к использованию!

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


Object.observe() и Object.unobserve()


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

// Модель может быть обычным нативным объектов
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
  });
}

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

Мы можем наблюдать за такими изменениями, используя O.o(), передавая в качестве первого аргумента наблюдаемый объект и в качестве второго возвращаемую функцию:

Object.observe(todoModel, observer);

Давайте попробуем что-нибудь сделать с нашей моделью:

todoModel.label = 'Buy some more milk';

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

Вау! Прощай, dirty-checking! Надпись на твоём надгробии будет высечена Comic Sans'ом. Давайте изменим другое свойство. В этот раз completeBy:

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

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

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

delete todoModel.completed;


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

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

Object.unobserve(todoModel, observer);

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


Фокусируемся на свойствах


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

Object.observe(obj, callback, opt_acceptList)

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

// Как мы и говорили выше, модель может быть просто нативным объектом
var todoModel = {
  label: 'Default',
  completed: false

};

// Мы можем указать возвращаемую функцию для любых
// изменений, происходящих с объектом
function observer(changes){
  changes.forEach(function(change, i){
    console.log(change);
  })

};

// С помощью которой мы в дальнейшем будем наблюдать объект,
// и конкретные события изменения, который нас интересует

Object.observe(todoModel, observer, ['delete']);

// Без третьего параметра мы будем получать уведомления
// о любых изменениях данного объекта

todoModel.label = 'Buy some milk'; 

// Обратите внимание, что события не возникает!

Теперь, если мы удалим свойство «label», нотификация об изменении возникнет:

delete todoModel.label;

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

Нотификации


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

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

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

image

А теперь давайте посмотрим на пример того, как нотификации могут быть использованы для оповещения об изменении состояния объекта. Обратите внимание на комментарии:

// Объявляем простую модель
var model = {
    a: {}
};

// И отдельную переменную, которую мы позже будем использовать
// в геттере нашей модели
var _b = 2;

// Определяем новое свойство "b" в нашем объекте "a"
// и устанавливаем ему геттер и сеттер
Object.defineProperty(model.a, 'b', {
    get: function () {
        return _b;
    },
    set: function (b) {

        // Когда свойство "b" установлено в модель,
        // мы должны оповестить мир о спецефических
        // изменениях, которые мы сделали. Это даёт вам
        // невероятный контроль над оповещениями
        Object.getNotifier(this).notify({
            type: 'update',
            name: 'b',
            oldValue: _b
        });

        // Давайте еще будем выводить значение каждый раз
        // когда что-то будет происходить
        console.log('set', b);

        _b = b;
    }
});

// Объявляем функцию наблюдения
function observer(changes) {
    changes.forEach(function (change, i) {
        console.log(change);
    })
}

// Начинаем наблюдать за изменениями model.a
Object.observe(model.a, observer);



Здесь мы выводим оповещение при изменении данных («update»), да и всего, чего угодно, собственно говоря, если это указано в вызове метода notifier.notifyChange().

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

Даже со стороны наблюдателя, вы не хотите, чтобы какая-то сторонняя функция была вызвана из середины текущей функции и т.п. Несогласованность и разобщенность мест вызовов функций раздражает, верно? И прибавьте к этому ещё проверки на ошибки, и прочие ситуации, которые могут усложнить жизнь при данном подходе. И в результате мы видим, что с подобной моделью реально трудно работать. Асинхронный подход сложнее в понимании, но всё же, на сегодняшний день, ничего лучше него нет.

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

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


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



Наблюдение за акцессорами и изменениями вычисляемых свойств может быть выполнено с помощью notifier.notify (это тоже включено в спецификацию Object.observe). Многие системы для наблюдения за изменениями должны предоставлять информацию об измененных значениях, и, честно говоря, у нас есть довольно много способов, как это сделать. Object.observe не навязывает нам «правильный» путь.

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

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

Давайте посмотрим, как наш код будет работать в 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 на самом деле не имеет возможности отслеживать изменения значения для акцессоров, ведь по факту они являются лишь коллекцией функций.

Если вы уже работали с акцессорами, то можете себе представить, как они работают: они просто предоставляет набор функций, которые дают доступ к свойствам, и ничего больше.

Наблюдение за несколькими объектами с одной возвращаемой функцией


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


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


Возможно, вы работаете над реально огрооооомным проектом и регулярно вынуждены сталкиваться с масштабными изменениями.
O.o() помогает в этом с помощью двух специфический функций: notifier.performChange() и notifier.notify(), о котором мы уже говорили.



Давайте посмотрим на пример того, как масштабные изменения могут бы описаны с помощью Thingy object с помощью некоторых математических функций (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);

    // Скажем системе, что коллекция работы включает в себя 
    // переданный тип изменения:
    // 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);
}


Что ж, теперь мы можем немного поиграться с кодом. Давайте объявим новый Thingy:

var thingy = new Thingy(2, 4);


Поставим его под наблюдение и сделаем несколько изменений. Охтыжмать, круто!
// Наблюдаем за thingy (простите, язык не повернется писать "наблюдаем за рюшечками")
Object.observe(thingy, observer.callback);
Thingy.observe(thingy, observer2.callback);

// Потыкаем методы, которые предоставляет нам thingy
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() — это метод, который работает с большим количеством изменений самого объекта, например splice, unshift или что-либо другое, что изменяет его длину, как, например, splice.
Для этого он использует 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';



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


О вычислительной скорости O.o() можно думать, как о скорости чтения кеша. Вообще говоря, кеш является отличным выбором, когда (в порядке важности):
  • Частота чтения выше, чем частота записи
  • Когда у вас есть возможность создать кеш, который будет жертвовать временем записи в сторону фиксированного времени на операции чтения
  • Постоянное время задержки для операций записи приемлимо

O.o() спроектирован для use-кейсов вроде первого.

Dirty-checking требует держать копии всех данных, за которыми вы наблюдаете. Это значит, что вы получаете просадку по памяти, которую вы никогда не получите с O.o(). Dirty-checking является своего рода затычкой, которая так же создает своего рода ненужную абстракцию, которая в результате создает лишние сложности в приложениях.

Почему? Потому, что dirty-checking запускается каждый раз, когда данные «могли» быть изменены. Это не является надежным способом подобной проверки и имеет существенные недостатки, например, гонку между кодом рендеринга (и т.п.) и вычислительным кодом (ведь все знают, что в JS используется один поток и для интерфейса и для вычислений?). Для dirty-checking так же требуется подключение глобального реестра наблюдателей, создавая тем самым риски утечки памяти и т.п., чего позволяет избежать O.o().

Давайте взглянем на некоторые цифры.

Представленные ниже бенчмарки (доступны на GitHub) позволяют нам сравни dirty-checking и O.o(). Они представлены в виде графа с абсциссой Observed-Object-Set-Size и ординатой Number-Of-Mutations.

Главные результаты — это то, что производительность dirty-checking'а пропорциональна количеству наблюдаемых объектов, в то время как производительность O.o() пропорциональна кол-ву мутаций, которые мы сделали.

Dirty-checking



Object.observe()



Object.observe() для старых браузеров


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

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

Пример наблюдения за значением по пути указанного объекта:
var obj = { foo: { bar: 'baz' } };

var observer = new PathObserver(obj, 'foo.bar');
observer.open(function(newValue, oldValue) {
  // Оповещаем, что у obj.foo.bar изменилось значение
});


2) Он будет оповещать вас об изменении длины массивов. Изменение длины массива в нашем случае — это минимальное количество splice-операций, которые мы должны сделать с массивом, чтобы перевести его из старого состояния в новое (изменившееся).

Пример оповещения о таких изменениях касательно массива, как минимальный набор splice-операций:

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

var observer = new ArrayObserver(arr);
observer.open(function(splices) {
  // Оповещаем об изменениях элементов массива
  splices.forEach(function(splice) {
    splice.index; // Позиция элемента, где возникло изменение
    splice.removed; // Массив значений, иллюстрирующий последовательность элементов, которые были удалены
    splice.addedCount; // Количество элементов, которые были добавлены
  });
});


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


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

Иегуда Кац и Эрик Брин из Ember утвердили поддержку O.o() в ближайших roadmap'ах Ember'а. Миско Херви (из Angular) так же написал в проекте своей документации к Angular 2.0 об улучшении детектирования изменений.
Yehuda Katz and Erik Bryn from Ember confirmed that adding support for O.o() is in Ember's near-term roadmap. Angular's Misko Hervy wrote a design doc on Angular 2.0's improved change detection. Я думаю, что наиболее вероятно ожидать движения в этом направлении, когда эта фича появится в пакете Chrome 36 stable.

Итоги


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

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

Что ж, теперь вы можете идти рассказывать авторам JS фреймворков об Object.observe() и как они могут использовать его, чтобы улучшить их механизм data-binding'а.
Похоже, грядут действительно удивительные времена!

Использованные ресурсы:




UPD: «Биндинг» превратился в «байндинг»
UPD 2: Комментарии были переведены на русский
UPD 3: Исправил грамматические и пунктуационные ошибки. Спасибо, Mingun
Поделиться публикацией

Похожие публикации

Комментарии 48

    +6
    O.o() — мощное нововведение для web платформы, которое вы можете использовать уже сегодня.


    Автор оригинала явно был слишком в восторге — иметь в виду — пожалуй, а ориентироваться на тех у кого Chrome 36 BETA было бы странно.

    P.S. О_о выглядит аутентичнее :)
      +2
      Мы надеемся, что это функциональность вскоре появится и в других браузерах, позволяя JavaScript фреймворкам получить некий выигрыш в производительности с новыми нативными возможностями объектов и наблюдением за ними. Помимо Chrome 36, эта функциональность так же будет доступно в ближайшем релизе Opera.

      Я надеюсь, что эта функциональность появится в стабильных версиях браузеров только тогда, когда стандарт утверждён, но не раньше, а то будет как с web sockets
        –2
        О мои глаза, они разрываются от слова «биндинг».
        А по теме, это же просто замечательно, основной недостаток использования шаблонов MV* в JavaScript заключался в потере производительности по мере усложнения дерева DOM и увеличения количества одновременных привязок, но теперь, с нативной поддержкой открываются совершенно новые горизонты.
          0
          О, а как вы его называете?
            0
            Привязкой (как переводится на русский) или байндингом (как произносится в английском), зависит от аудитории :)
              –2
              Вы правы, исправил. Спасибо за замечание!
                +1
                А слово «дата» вас не смущает? Может уж сразу, «дэйта-байндинг»?
                0
                Мне глаза не рвет, но я называю это «байндингом» или, иногда, «привязкой».
                По теме поддержу — очень многообещающая штука, надеюсь утвердят как можно скорее!
                +1
                Да нет, MV* != dirty checking
                Честно говоря, не представляю, как вообще Angular приобрел популярность с такой жуткой штукой под капотом.
                0
                По-моему уже было на Хабре.
                  +1
                  Были похожие статьи(поверхностное овервью), но перевода именно этой небыло.
                  +3
                  Похоже на Key-Value Observing в Objective-C
                    +1
                    Господа переводчики, ну что за привычка игнорировать текст в комментариях? Поверьте, он ничем не отличается от текста переводимой статьи. Так почему же вы забиваете на его перевод? Что это за дурацкое табу? Особенно показателен случай в этой статье, когда после фразы «Обратите внимание на комментарии:» следует полотно кода с не переведёнными комментариями. А-а-а-а! Хочется биться головой об стену в таких случаях. Если уж переводите, делайте, пожалуйста, это полностью. Очень всех прошу.

                    P.S. Извините, но накипело.
                      –2
                      Наверно дело привычки, но для меня дико выглядят комментарии на русском языке даже в примерах, не говоря уже о рабочем коде.

                      По теме — функционал конечно полезный. Привыкнув к привязке при работе с AngularJs приходится перестраиваться, когда не можешь ее использовать.
                        +2
                        Простите, но именно в примерах их и надо переводить. Иначе зачем вообще нужны примеры? Вы же не будете их бездумно копипастить, не так ли? И никто же не заставляет вас писать по-русски в рабочем коде (хотя, если проект только для внутреннего использования, почему бы и нет). А здесь, простите, рассказывают о новой технологии, и эти самые комментарии являются продолжением текста статьи. Я уже привёл пример: фраза «Обратите внимание на комментарии:». Это значит, что автор считает, что в комментариях описываются некоторые нюансы, которые при переводе оказались безвозвратно утерянными.
                          0
                          // Наблюдаем за thingy(простите, язык не повернется писать «наблюдаем за рюшечками»)

                          Один из примеров когда переводить комментарии не лучшая идея. Тогда не плохо бы переменные транслитерировать. Кстати изображения без перевода вас не смущают?
                            0
                            Ну, я все-таки согласен с Mingun по поводу перевода комментариев. Разумеется, в жизни такого делать не стоит, но ведь это обычный обзор, и чем понятнее — тем лучше. Было бы неплохо переводить и изображения (например, на схемах), но это, может быть, где-нибудь в следующих переводах.
                              0
                              Кстати изображения без перевода вас не смущают?

                              Знаете, требовать от переводчика еще и перевода изображений — даже для меня это чересчур :) Что впрочем, не отменяет того факта, что если это будет сделано, то это будет просто о*утельно здорово! Всё-таки перерисовка изображений не каждому под силу, объём работ не сравнимый.
                          0
                          Полностью переводить комментарии — дело нелегкое.

                          MyApp.country.get(«presidentName»);
                          // «Barack Obama»
                            0
                            Mingan Согласен, это было моим упущением, исправил. Можете наслаждаться переведенными комментариями!
                              0
                              Спасибо! Так действительно намного лучше. И статья даже читается как-то более на одном дыхании что ли :)
                            0
                            Ни одного примера про reconfigured.
                              0
                              А годные полифилы имеются? Гитхаб-сечинг выдал только этот, который не выглядит особенно рабочим.
                                0
                                Тут можно сделать только через свой особый объект. И ставить все свойства через метод set, remove. Всё остальное будет по скорости проигрывать как на диаграмме выше.
                                  0
                                  Можно еще использовать getter и setter, тогда будет выглядить как объект (я пробовал — работает =). Но тут опять же мы фиксируем «схему» объекта.
                                    +1
                                    А через сет\гет получается как раз обходной полифил, который будет работать в IE 5. Ext.js пошли по этому пути очень давно.
                                      0
                                      Ага, сам таким пользуюсь, когда ie8 нужен.
                                  0
                                  Пока не нашел ничего кроме полимеровского(который описан в статье)
                                  +1
                                  Без вменяемых полифилов для ИЕ всё это пока дела будущего.
                                    0
                                    Я не могу себе представить полифил для такого функционала. Невозможно отследить изменения свойства объекта не через акцессор. Думаю, как только спецификация будет окончательно утверждена, в ближайших на тот момент версиях появится поддержка такого функционала, даже в IE(разумеется, речь идет только об edge-версиях)
                                      +3
                                      Автора переполняют эмоции, хотя если разобраться, то окажется, что `Object.defineProperty` уже давно удовлетваряет всем желаниям. Единственное не возможно следить за добавлением/удалением свойств, но это крайне редкая необходимость по двум простым причинам:
                                      1* Архитектурная — модели и классы должны быть полноценны и однозначны
                                      2* Производительная
                                      — добавляя-удаляя свойства обьекта, V8 как минимум, пересоздает скрытые классы для обьекта
                                      — создается небольшой event flood, так как нужно фильтровать `changes`, что бы слушать только например `foo property update`
                                      В наших проэктах давно уже завели себе Object.[add/remove]Observer Object.[lock/unlock]Observers которые добавляют ака proxy функции для сеттеров и ака breadcrumbs proxies для вложенных обьектов, как с примера для полимера.
                                      var obj = { foo: { bar: 'baz' } };
                                      Object.addObserver(obj, 'foo.bar', console.log.bind(console));
                                      obj.foo.baz = 'qux';
                                      //> qux
                                      obj.foo = { bar: 'quxx' }
                                      //> quxx
                                      


                                      И ещё, `getNotifier` отлично заменяется `EventEmitter`ом. А что бы следить за массивом, достаточно обернуть его мутаторы, сново же, своим proxy методом.

                                      Я никаким образом не хочу сказать, что O.o плох — напротив, и статья отличная, только много из этих вещей можно и нужно использовать уже сейчас! Ну, кто может себе позволить IE9+;
                                        0
                                        Я тоже делал похожую вещь, для своего проектика (тут если интересно).
                                        Пример из readme
                                        var model = require('moco').model;
                                        
                                        var User = model()
                                          .attr('_id', { primary: true }) // this attribute will be used as primary key
                                          .attr('name') // just simple attribute
                                          .attr('email', { get: function() {
                                            return this.name + '@example.com';
                                          }}) // this is a getter and it called each time you try access this model
                                          .attr('title', { default: 'Unknow' })// this will be used to fill undefined attribute while creation
                                        
                                        var u1 = new User({ _id: 1, name: 'den'});
                                        console.log(u1._id); // 1
                                        console.log(u1.primary); // 1
                                        
                                        console.log(u1.email); // den@example.com
                                        console.log(u1.title); // Unknow
                                        
                                        u1.name = 'daniel';
                                        
                                        console.log(u1.email); // daniel@example.com
                                        
                                        +2
                                        Только функциональность, а не функционал. Специально не пишу в личку, ибо в последнее время болезнь приобрела массовый характер (впрочем, ощущение, что уже пора смириться).
                                          0
                                          Исправлено
                                          +6
                                          Революция! Грядёт революция! Она изменит всё! Ветер перемен! Она изменит всё, что вы знали о дата-байндинге! Она полностью изменит подход! Саму концепцию! Вы никогда раньше такого не видели!!! Вы больше не увидите старого джаваскрипта!!! Вы готовы?! Вас ждёт погружение в новый мир! Мир, в котором на каждой строчке используется слово дата-байндинг! Вы готовы?! Я спрашиваю, ВЫ ГОТОВЫЫЫ!!!???
                                            0
                                            Как «автор фреймворков» не вижу особого повода для радости.
                                            1. Многие всё ещё поддерживают ie8 где даже акцессоров толком нет.
                                            2. В сравнении с ангуляровскими грязными проверками любой фреймворк выглядит как манна небесная.
                                            3. Никто находясь в трезвом уме не будет светить полями наружу — только через акцессоры. Соответственно вся подписка/отписка/нотификации будут делаться как и сейчас — вручную. Новое апи тут вносит лишь неоднородность реализации не давая ощутимого профита.
                                            4. Единственное полезное применение — отслеживать изменение в чужих объектах. Особенно полезно это было бы для хостовых объектов. Но о них в статье ни слова и что-то мне подсказывает, что с ними будет всё как обычно — плохо.
                                            5. Никак не решена проблема холостых вычислений. Например, переменная А зависит от C и B, а B зависит от C. В результате когда изменится С сначала будет вычислена А, потом В, а потом снова А. В общем с этим АПИ будет та же беда, что и с нокаутом:
                                            диаграмма: nin-jin.github.io/slide/#slide=induction
                                            тест: jsperf.com/reactive-libraries-comparison
                                            6. Они это апи точно согласовали с остальными игроками? А то получится, что в хроме Object.observe, в мозилле watch, а ie вообще не при делах.
                                              0
                                              5. A зависит от C и B, B зависит от C. Когда изменится C, без разницы, что будет изменено раньше, A или B, т.к. событие произойдет только после обработки обоих микрозадач. В крайнейм случае, если я ошибаюсь (а это вряд ли, см. статью), всегда можно перегрузить акцессор и создавать свои нотификации только в том случае, если оба свойства (B и C) будут изменены.
                                              0
                                              Даже со стороны наблюдателя, вы не хотите, чтобы какая-то сторонняя функция была вызвана из середины текущей функции и т.п. Несогласованность и разобщенность мест вызовов функций раздражает, верно? И прибавьте к этому ещё проверки на ошибки, и прочие ситуации, которые могут усложнить жизнь при данном подходе. И в результате мы видим, что с подобной моделью реально трудно работать. Асинхронный подход сложнее в понимании, но всё же, на сегодняшний день, ничего лучше него нет.


                                              Здесь с автором не соглашусь. В большинстве случаев, как раз, мы хотим, чтобы все изменения обрабатывались сразу же. Если вы меняете поле A, то вы хотите, чтобы поле B = A + C изменилось сразу же. Если вы добавляете элемент в массив данных, то вы хотите, чтобы элемент добавился и в массив представлений сразу же. Иначе пришлось бы запускать таймер на 1 мс, чтобы дождаться рендеринга представления, или использовать другой костыль, если вам нужно обратиться к этому представлению. Помню, часто сталкивался с такой проблемой, когда программировал на ExtJS 2.0 — изрядно бесило. И обработка ошибок с синхронным подходом куда проще — в стек-трейсе сразу видно, где косяк.

                                              Единственная причина использовать отложенные нотификации — это ликвидация многократного пересчета одного и того же значения с целью оптимизации. Например, если пользователь кликает по кнопке «выбрать все записи», мы не хотим, чтобы свойство «выбрана ли хотя бы одна запись?» пересчитывалось N раз.

                                              Согласен со всеми комментариями: область применения данного нововведения весьма и весьма ограничена. Не понятно, зачем ради такой мелочи расширять стандарт ECMAScript, ведь фреймворки итак замечательно решают эту задачу, притом без видимого влияния на производительность (dirty check не в счет). И вообще, API не идеален: с таким event flood все будет тормозить еще больше.
                                                +1
                                                Это отличная возомжность реализовать нативный дата-байндинг. О каком event flood вы говорите? Вы с такой скоростью меняете данные? Даже если и так, то каждый раз, когда мы двигаем мышь, так же возникают события. Я не вижу ничего плохого в расширении ES, тем более, что Вы, скорее всего, даже не заметите, как поменяется начинка всех современных фреймворков в сторону О.о
                                                  0
                                                  Как я описал выше — будут лишние вызовы обработчиков, от которых придётся избавляться дополнительными костылями, которые будут буферизировать события.

                                                  Кстати, куда можно написать, чтобы предложить разработчикам браузеров реализовать более полезное апи?
                                                    0
                                                    Я думаю, что тут вы сможете найти всю интересующую вас информацию.

                                                    Как я описал выше — будут лишние вызовы обработчиков

                                                    Я сильно извиняюсь, но я до сих пор не понимаю, о чём вы говорите. Если данные меняются блочно, например, через сеттер, то оповещение возникнет только в конце очереди микрозадач, т.е. когда все N свойств изменятся. Если изменяется одно свойство — оповещение происходит сразу. Опишите пожалуйста кейс, когда возникнут лишние вызовы?
                                                      0
                                                      Я хочу прослушивать только update у поля foo. Для этого мне надо прослушивать все изменения. Независимо от того, что произошло, и в каком поле объекта, обработчик будет срабатывать. Получается такой нелепый код.

                                                      var model = {
                                                          foo: 1
                                                      };
                                                      Object.observe(model, function(changes) {
                                                          changes.forEach(function(change) {
                                                              if (change.type === "update" && change.name === "foo") {
                                                                  console.log(change.type, change.name, change.oldValue);
                                                              }
                                                          });
                                                      });


                                                      Это и называется event flood.
                                                        0
                                                        Извините, если разочаровал :)

                                                        var model = {
                                                            foo: 1
                                                        };
                                                        Object.observe(model.foo, function(ev) {
                                                            if (ev.type === 'update') {
                                                                console.log(ev.type, ev.name, ev.oldValue);
                                                            }
                                                        });
                                                        
                                                          0
                                                          http://wiki.ecmascript.org/doku.php?id=harmony:observe_public_api, пункт 1: If Type(O) is not Object, throw a TypeError exception.

                                                            0
                                                            var obj = { foo: { bar: 'baz' } };
                                                            
                                                            var observer = new PathObserver(obj, 'foo.bar', ['update']);
                                                            observer.open(function(newValue, oldValue) {});
                                                            

                                                            Хотя это только через полифил :(
                                                              0
                                                              Ну вот, а если полифил опирается на этот стандарт, то где-то там внутри он фильтрует этот самый event flood. То есть вычислительная сложность такого API далеко не оптимальна. Надеюсь, tenshi сумеет убедить консорциум исправить этот косяк ;)
                                                0
                                                Отличная статья, но не хватает чуточку информации о привязке к DOM… Не сочтите за наглость мою просьбу немного дополнить и пояснить как использовать O.o(), например, для li элементов?
                                                  0
                                                  Похоже что Object.observe отжил, авторы хотят вырезать его (переписка, сообщение).
                                                  Предположительные причины — медленный (Object.defineProperty работает быстрее*) и сложности использовать в фреймворках (например его пытались внедрить в Angular 1, а в Angular 2, не смотря на обещания, его по прежнему нет).
                                                  На данный момент Object.observe используется в Polymer.js* и Angular Light.

                                                  Кроме того «на подходе» Proxy из ES6 (который уже есть* в FF и IE, а так же в Chrome но с флагом).

                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                  Самое читаемое