Последние несколько месяцев в сети шли бурные дебаты о наиболее удачных способах обработки событий. Сначала Google выпустил библиотеку JsAction, а затем был анонсирован метод Object.observe(), который будет реализован в стандарте ECMAScript 7, но уже поддерживается в Chrome 36 и Node.js Harmony.

Разработчики разделились во мнениях: обязательно ли сохранять всю логику в файлах JS или же допустимо вставлять отдельные части логики в HTML. В этой статье мы попробуем разобраться в этих спорах, рассмотрев различные паттерны для обработки ошибок и затем взвесим за и против у всех альтернатив.

Факты

JsAction это библиотека Google для делегирования событий в JS. Она основана на библиотеке Closure и впервые была применена в Google-картах пару лет назад для решения проблем с ошибками обработчиков событий в некоторых браузерах. JsAction нацелена на отделение событий от методов их обработки и с этой целью переносит часть логики обработчиков событий в HTML.

Вообще, это один из трендов в веб-разработке — переносить часть логики не просто в HTML, а внутрь DOM-элементов, затрагиваемых логикой. И это относится не только к обработке событий, есть основанные на шаблонах фреймворки (Angular, Ractive, React), они реализуют паттерн Модель-Представление-Контроллер в веб-приложениях, позволяя связывание данных и реактивное программирование.

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

Исторический экскурс

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

Вот как выглядело изменение фонового цвета страницы по нажатию мышки в те времена:

<button onclick="document.bgColor='lightblue'">Feel Blue</button>

Прошло немного времени до того как ограничения и опасности использования JS в атрибутах HTML-тегов были выявлены. В ноябре 2000 года в стандарт ECMAScript 3 был включен новый способ привязывания обработчиков к событиям браузера. К этому моменту компания Microsoft уже реализовала в своем браузере метод attachEvent(), но прошло время, прежде чем эти методы начали использоваться. Только спустя 4 года после первого упоминания (разработчиками Netscape Navigator) концепция ненавязчивого JS начала распространяться.

Разработчики в Netscape Navigator считали, что прослушивание событий устраняют следующие недостатки инлайновых обработчиков событий:

  • Смешивание кода и разметки делает код менее читаемым и затрудняет его поддержку.
  • Загрязнение глобального пространства имен: весь инлайновый код определяется в глобальном пространстве, соответственно, каждая функция обращающаяся к этим обработчикам также должна быть глобальной.
  • Это мишень для XSS-атак, в атрибуте может быть любой код, в том числе вредоносный, который можно передать в eval без всякой проверки.

В 2006 году, с выпуском первых библиотек, ориентированных на широкое использование Ajax, таких как YUI и Jquery, подход с использованием прослушивания событий был применен без всякого ожидания и это позволило ускорить внедрение передовых практик, что в итоге к ним пришло большинство разработчиков.

Тогда же к методу прослушивания событий были добавлены:

  • Масштабируемость: инкапсуляция обработчика событий внутри функции соответствует принципу DRY, так как позволяет работать на уровне прототипа объекта и использовать одинаковую логику для множественных обработчиков; а с помощью механизма селекторов JQuery CSS дает легкий и эффективный способ привязывания обработчиков к набору узлов:

$(document).ready(function () {
  $('.clickable').click(function () {
    document.body.style.background='lightblue';
    return false;
  });
});
  • Отладка: с использованием таких инструментов как Firebug и Chrome Developer Tools отладка JS становится меньшим кошмаром, по сравнению с отладкой инлайнового кода.

Проблемы с паттерном addEventListener

Подход с прослушиванием событий, несмотря на свои очевидные плюсы имеет и недостатки:

  • Прикрепление слушателей к объектам может вести к утечкам памяти в замыканиях, если сделано с ошибкой. Замыкания это одна из наиболее мощных возможностей JS, но использовать их надо с осторожностью, когда они переплетаются с DOM-элементами. В замыканиях имеется указатель на их область видимости. В результате, прикрепление замыкания к DOM-элементу может вызвать круговое обращение к нему и, соответственно, утечку памяти. Вот пример из руководства Google по стилю написания JavaScript, с образцами правильной и неправильной реализаций.
  • В ИЕ также возникает проблема с уборкой мусора, особенно, когда дело доходит до событий. Кроме общеизвестной проблемы взаимных циклических ссылок, старые версии ИЕ не удаляю обработчик после удаления элемента DOM, что влечет дополнительные утечки.

Еще раз: что такое JsAction?

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

Каждый обработчик событий отдельно регистрируется в одном или нескольких JS файлах или скриптах; они ассоциированы с названиями методов, а так как связь между именами и функциями берет на себя библиотека, нет необходимости добавлять их в глобальный контекст.

В целом, использование JsAction должно дать следующие выигрыши:

  1. Решение проблемы утечки памяти в старых браузерах.
  2. Предотвращение или уменьшение загрязнение глобального пространства имен.
  3. Уменьшение зависимости обработки событий от конкретной реализации в браузере.
  4. Лучшая производительность и масштабируемость, позволяющая установить один прослушиватель событий на странице и затем через него маршрутизировать события к их обработчикам.

Образец работы JsAction можно увидеть на странице библиотеки на GitHub.

Честно говоря, код в примере не слишком легок для понимания и не настолько прост как можно было ожидать. А большая часть его результатов может быть достигнута несколькими строками кода. Загрязнение глобального пространства имен может быть ограничено при использовании паттернов “модуль” или “пространство имен”. Отложенная загрузка легко достижима с помощью установки в начале заглушек вместо обработчиков событий, а затем асинхронной загрузкой внешнего скрипта с реальными обработчиками и переназначением на них событий.

Реализация пунктов 3 и 4 более сложна: нам необходимо установить единственный обработчик для всей страницы, установить атрибут в элементах DOM, установив, какой метод мы будем использовать в качестве обработчика и создать “супер-обработчик”, который будет маршрутизировать события к обработчикам.

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

  1. Библиотека не относиться к легковесным.
  2. Она не выглядит особо интуитивной в использовании, и кривая обучения может быть достаточно крутой для начинающих. Документация по ней на данный момент явно не достаточная.
  3. Начало работы с ней может быть достаточно трудным. В отсутствие готовой скомпилированной версии необходимо в начале скачать библиотеку Closure с компилятором и скомпилировать JsAction.

Декларативные фреймворки

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

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

Такие фреймворки как RactiveJs, Angular, Ember, and React не просто позволяют встраивать код в представления. Они активно используют модели, основанные на шаблонах, что позволяет связывать обработчики событий, данные и логику представления внутри элементов DOM, а затем специализировать эту логику в различных скриптах. В принципе, та же схема используется в JsAction для разделения имен обработчиков событий и их имплементации. В итоге они скорее увеличивают разделение между логикой и представлением, выводя приложения на основе MVC на более высокий уровень и параллельно упрощая использование шаблонов.

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

С этой же целью, Ractive и React используют виртуальный DOM — абстрактное представление DOM, которое позволяет проводить очень быстрые операции за счет сокращения необходимых манипуляций с DOM. Они очень похожи между собой, фокусируясь на реактивном программировании и визуализации. В отличие от Angular, который не фокусируется на представлениях, а является более комплексным фреймворком, позволяя обрабатывать маршрутизацию, соединение с сервером и т.д.

Все эти фреймворки поддерживают двухстороннее связывание, удобный способ для достижения согласованности между состоянием DOM элементов и логики приложения. Например, если вам нужно вывести на экран список чего-либо. В традиционной императивной парадигме вам нужен примерно такой код:


<!doctype html>
<html>
  <body>
    <div id="container" class="container" >
    </div>
    <script type="text/javascript" src="..."></script>
  </body>
</html>


function createItemHTML (val) {
  return '<span class="">' + val + '</span>';
}

function displayList (container, items) {
  container.empty();
  $.each(items, function (index, val) {
    var element = $('<div>');
    element.attr('id', 'div_' + index);
    element.html(createItemHTML(val));
    container.append(element);
  });
}

function editItem (container, itemId, itemValue) {
  var element = container.find('#' + itemId);
  if (element) {
    element.html(createItemHTML(itemValue));
  }
}
//...
displayList($('#container'), items);
//...
editItem(container, id, newVal);

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

А вот как выглядит решение той же задачи средствами Ractive:


<!doctype html>
<html>
  <body>
    <div id="container" class="container" >
    </div>
    <script src="http://cdn.ractivejs.org/latest/ractive.js"></script>
    <script src="logic.js"></script>
    <script id='listTemplate' type='text/ractive'>
      {#items:num}
        <div id="div_" on-click="itemClick">
          <span></span>
        </div>
      {/items}
    </script>
  </body>
</html>


var ractive = new Ractive({
  el: 'container',
  template: '#listTemplate',
  data: {
    'items': items
  }
});

ractive.on({
    'itemClick': function (e) {
      //access e.node and e.context for both the DOM element
      //  and the Ractive state associated with it
    }
});

//...

//Now update items with a new list
ractive.set('items', newItemsList);

Все! Нет необходимости писать код для обновления страницы. Ractive сделает это за вас. Она яснее, поддерживаемее, лучше спроектирована и более производительна. Мы также можем добавлять обработчики событий на лету.

Object.observe()

Object.observe() это взгляд в будущее, потому как его даже нет в 6-ой спецификации EcmaSript, он есть только в новейшей 7-ой версии. Однако компания Гугл уже почти реализовала его в последней версии своего браузера (Chrome 36), а библиотека Polymer Observe-JS предлагает полифилл для этого метода в современных браузерах, до тех пор, пока в них не появится нативная поддержка.

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

Связывание данных в декларативных фреймворках

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

Другой способ, применяемый в Ember, Backbone и Ractive это использование объекта-контейнера. фреймворк создает объекты в которых хранятся данные. В этих объектах есть методы доступа к данным, и каждый раз при запросе или изменении данных, фреймворк захватывает ваше действие и распространяет сведения о нем подписчикам. Это решение отлично работает, более производительно, чем “грязный контроль” и лучше масштабируется.

Улучшение производительности

Новый метод позволяет наблюдать за объектом, изменять его свойства и получать отчет об этом. Применение его может выглядеть так:

// A model can be an object literal
var plainObject = {
  name: 'Counter',
  total: 0
};

// Define an observer method
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
  });
}

// Start watching the object
Object.observe(plainObject, observer);

Прекращение наблюдения за объектом делается в одну строчку кода.


Object.unobserve(plainObject, observer);

План развития

Как я уже написал, нативная поддержка Object.observe() реализована в Chrome 36 и в Node.js с включенным флагом harmony. Также реализация предусмотрена в ближайших версиях Opera. В остальных браузерах можно пока использовать полифилл на Observe-JS Polymer library, что также дает поддержку некоторых старых браузеров.

В декларативных фреймворках также ожидается поддержка этого метода — в Ember и Ractive она запланирована в ближайших релизах, в Angular с выходом мажорного релиза.

Заключение

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

Дополнительные материалы: