Pull to refresh

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

Reading time 17 min
Views 38K
Original author: Addy Osmani

Введение


Грядет революция. Появилось новое дополнение к 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
Tags:
Hubs:
+49
Comments 48
Comments Comments 48

Articles