В течение последних нескольких месяцев в сети проходили дебаты о том, как лучше всего справляться с событиями. Во-первых, несколько месяцев назад Google выпустил библиотеку JsAction; затем, совсем недавно, метод Object.observe()
был представлен как часть спецификации ECMAScript 7 (но уже поддерживается в Chrome 36 и Node.js Harmony ).
Разработчики уже придерживались мнения о том, все еще ли «обязательно» ограничивать всю логику файлами сценариев, или приемлемо или даже предпочтительнее встроить отдельные части этой логики в HTML. В этой статье мы попытаемся разобраться в этой дискуссии, пройдемся по различным шаблонам обработки ошибок, а затем рассмотрим плюсы и минусы этих альтернатив.
Факты
JsAction — это библиотека Google для делегирования событий в JavaScript. Он основан на Closure Library и был представлен на картах Google несколько лет назад, чтобы устранить некоторые ошибки браузеров, связанные с управлением прослушивателями событий. JsAction стремится отделить события от методов, обрабатывающих их, и для этого перемещает часть логики обработки событий в HTML.
В последнее время появилась общая тенденция, которая перемещает часть логики не только в файлы HTML, но и в элементы DOM, на которые воздействует эта логика. Это не верно только для обработки событий: появляется ряд основанных на шаблонах фреймворков (таких как Angular, Ractive, React); они применяют шаблон Model-View-Controller в веб-приложении и позволяют связывать данные и реагировать на программирование.
Введение метода Object.observe()
в следующую спецификацию ECMAScript — это еще один шаг в этом направлении, поскольку он позволяет разработчикам естественным образом применять шаблон Publisher / Subscriber к целому новому набору ситуаций, а не только к обработке событий. Декларативные структуры уже основаны на этой логике, но введение Object.observe()
поможет им добиться удивительного улучшения производительности.
История до сих пор
С момента появления JavaScript ортодоксальный способ обработки событий несколько раз менялся. Изначально, если вы хотели добавить динамическое поведение к элементам на вашей странице, у вас был только один способ: добавить атрибут к самому тегу и связать с ним фрагмент кода JavaScript. Вы можете либо написать код внутри значения атрибута, либо вызвать одну или несколько функций, ранее определенных в глобальной области видимости.
Например, чтобы изменить фон вашей страницы на синий с помощью нажатия кнопки:
<button onclick="document.bgColor='lightblue'">Feel Blue</button>
Вскоре были обнаружены ограничения и опасности HTML on[event]
атрибутов on[event]
. С ноября 2000 года метод addEventListener был добавлен в спецификацию ECMAScript 3 в качестве альтернативного способа привязки обработчиков к событиям браузера. Ранее Microsoft уже добавила метод attachEvent()
, но потребовалось некоторое время, чтобы завоевать attachEvent()
. Несмотря на то, что в начале 2000-х годов в сети распространялись слухи, примерно через 4 года после этого появился термин « ненавязчивый JavaScript» .
Подход Netscape, который встроенные обработчики событий, действительно, имели некоторые недостатки, которые решает подход прослушивателя событий:
-
Смешивание кода и разметки может сделать ваш код менее читаемым и гораздо менее поддерживаемым.
-
Глобальное загрязнение области: встроенный код определяется в глобальной области, и каждая функция, вызываемая в нем, также должна быть определена в глобальной области.
-
Это слабое место для внедрения 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 , отладка JavaScript стала менее кошмарной, но встроенный код разрушил бы все это.
Проблемы с шаблоном addEventListener
Тем не менее, подход к слушателю событий вызвал серьезные опасения:
-
Присоединение слушателей к объектам в JavaScript может привести к утечкам закрытия, если это не сделано должным образом. Замыкания являются одной из самых мощных языковых функций JavaScript, но их следует использовать с осторожностью, когда они связаны с элементами DOM. Замыкания сохраняют указатель на их объем. В результате прикрепление замыкания к элементу DOM может создать круговую ссылку и, таким образом, утечку памяти. Этот пример из руководства по стилю JavaScript Google показывает правильный и неправильный способ борьбы с ним.
-
В Internet Explorer была довольно проблематичная обработка сбора мусора, особенно когда речь шла о событиях. Помимо хорошо известной проблемы взаимных циклических ссылок , в старых версиях браузера Microsoft, когда узел удалялся из DOM, его обработчики не собирались мусором, и это вызывало утечки памяти.
Что опять за действие?
Это приводит нас прямо к JsAction. Как упоминалось в начале этого поста, это библиотека делегирования событий, которая позволяет отображать события и обработчики по их именам, используя специальный атрибут HTML с именем jsaction
, который будет непосредственно обрабатываться библиотекой.
Каждый обработчик событий отдельно регистрируется в одном или нескольких файлах JavaScript или встроенных сценариях; они связаны с именами методов, и поскольку отображение между именами и функциями осуществляется самой библиотекой, нет необходимости добавлять их в глобальную область видимости.
Таким образом, JsAction должен предоставить несколько преимуществ:
1. Обойти проблемы утечки памяти в некоторых (старых) браузерах;
2. Уменьшить или избежать глобального масштабного загрязнения;
3. Уменьшить связь между событиями и реализациями обработчиков;
4. Лучшая производительность и масштабируемость, поскольку она позволяет установить одного прослушивателя событий на страницу, а затем направить события непосредственно в соответствующий обработчик;
Чтобы увидеть пример того, как это работает, проверьте страницу GsHub JsAction .
По правде говоря, пример кода не так легко читается и не так прост, как вы ожидаете. Кроме того, большинство из вышеперечисленных свойств можно получить с помощью нескольких строк JavaScript. Например, загрязнение глобальной области действия может быть ограничено с помощью шаблонов модуля и пространства имен . Поздняя загрузка может быть легко достигнута путем первоначального назначения заглушек обработчикам событий, затем асинхронной загрузки внешнего сценария с реальными обработчиками и повторного сопоставления событий по завершении.
Реализация пунктов 3 и 4 немного сложнее: нам нужно установить один обработчик для всей страницы, установить атрибут в элементах DOM, указывающий, какой метод будет использоваться в качестве обработчика, и создать метод «супер-обработчика», который маршрутизирует рабочий процесс в соответствующий метод.
Еще раз, это может или не может быть правильным решением для ваших нужд, в зависимости от характеристик вашего проекта. Несмотря на свои плюсы, у него все еще есть слабость:
-
Библиотека не совсем легкая.
-
Это не выглядит особенно интуитивно понятным в использовании, и кривая обучения, вероятно, будет крутой для начинающих. Документация тощая, и это не помогает.
-
Это может быть трудно начать с этим. Если скомпилированная версия недоступна, вы вынуждены загрузить компилятор Closure и библиотеку Closure.
Декларативные рамки
Таким образом, JsAction не может быть окончательным решением для обработки событий в JavaScript, и, как мы видели, он существует уже некоторое время, хотя и не как проект с открытым исходным кодом. И все же, после того, как это было с открытым исходным кодом, в сети начались оживленные дебаты между энтузиастами и критиками. Помимо врожденной любви к огню интернет-поколения, я полагаю, что одной из основных причин, вероятно, является тот факт, что декларативные структуры, популярность которых быстро растет, в значительной степени имеют один и тот же выбор дизайна, с более высокой степенью интеграции между представлением и логикой и возврат к встроенному коду не только для обработчиков событий, но даже для заполнения элементов страницы содержимым.
Подожди, разве смешивание логики и презентации не плохо? Ну, это так! Мы упомянули несколько преимуществ отделения вашей логики от презентации, простоты отладки и ясности. Но иногда ремонтопригодность может быть улучшена путем указания логики, связанной с объектом рядом с самим объектом.
Такие фреймворки , как RactiveJs , Angular , Ember и React, предназначены не только для того, чтобы позволить вам внедрять код в ваши представления. Они широко используют модели на основе шаблонов для представления, чтобы позволить вам связывать обработчики событий, данные и даже логику представления непосредственно внутри элементов DOM, а затем указывать детали этой логики в отдельных сценариях. По сути, это та же схема, которая используется JsAction для разделения имен обработчиков событий и реализаций обработчиков. В целом, они скорее увеличивают разделение между представлением и логикой, обеспечивая более высокое применение шаблона MVC , и в то же время позволяют очень удобно использовать шаблоны.
Эти рамки контролируют гораздо больше, чем обработка событий. Они также позволяют привязку данных, что становится важным, когда вы заботитесь о разделении Model-View-Controller. Они позволяют вам связывать части представления с объектами JavaScript, обновляя его каждый раз, когда объект за ним изменяется. Более того, они обновляют представления особенно эффективными способами, изменяя только самые маленькие узлы DOM, затронутые изменением, ограничивая перерисовку страницы, поскольку это будет узким местом в большинстве веб-приложений.
Для этого Ractive и React используют виртуальный DOM — абстрактное представление DOM, которое позволяет выполнять очень быстрые операции, сводя к минимуму количество манипуляций с DOM, которое необходимо выполнить. Они очень похожи друг на друга, и фокусируются на реактивном программировании и визуализации. Хотя Angular не просто сфокусирован на части представления MVC, он представляет собой более сложную инфраструктуру, которая одновременно обрабатывает маршрутизацию, подключение к серверу и т. Д.
Все эти платформы поддерживают двустороннее связывание , удобный способ обеспечить согласованность между значениями в 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_{{num}}" on-click="itemClick"> <span>{{this}}</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()
— это взгляд в будущее, так как он даже не вошел в спецификацию ES6 — он только что был добавлен в ES7. Однако Google уже внедрил его в Chrome 36, и библиотека Observe-JS Polymer будет имитировать его поддержку в каждом браузере, используя встроенную поддержку, когда она доступна.
Этот метод позволяет асинхронно наблюдать за изменениями объектов и массивов. Наблюдатели получат упорядоченные по времени последовательности записей изменений, описывающих набор изменений, которые произошли в наборе наблюдаемых объектов. При использовании Object.observe()
событийно-ориентированное программирование, также известное как реактивное программирование, больше не ограничивается пользовательским интерфейсом. Например, вы можете реализовать двустороннее связывание данных с языковыми примитивами — для этого не нужно устанавливать фреймворк, такой как Ractive.
Привязка данных в декларативных рамках
Одним из решений для обеспечения привязки данных является грязная проверка (используется Angular). В любое время, когда данные могли измениться, библиотека должна пойти и проверить, действительно ли она изменилась, используя цикл дайджеста или цикл изменения. Цикл дайджеста Angular идентифицирует все зарегистрированные выражения для просмотра и проверяет, есть ли какие-либо изменения.
Другое решение, используемое Ember, Backbone и Ractive, — это использование контейнерных объектов. Каркас создает объекты, которые содержат данные. Эти объекты имеют средства доступа к данным, и поэтому каждый раз, когда вы устанавливаете или получаете какое-либо свойство, платформа может зафиксировать ваше действие и внутренне передать его всем подписчикам. Это решение работает хорошо и относительно эффективно по сравнению с грязной проверкой, с хорошим алгоритмическим поведением, пропорциональным количеству измененных вещей.
Улучшение производительности
Новый метод, добавленный к языку, позволяет нам наблюдать за объектом, изменять свойства и видеть отчет об изменениях. Если вы хотите посмотреть обычный объект JavaScript, это так просто:
// 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 и в nodejs Harmony (включите ее с флагом --harmony
). Также сообщается, что Opera работает над поддержкой поддержки связывания данных в одном из следующих выпусков. В то же время, ожидая, когда другие браузеры догонят вас, вы можете использовать библиотеку Observe-JS Polymer, чтобы гарантировать, что ваше приложение будет работать даже со старыми версиями браузеров.
Как вы можете себе представить, даже декларативные платформы в среднем восприняли это как возможность: Ember и Ractive планируют выпустить полную поддержку Object.observe()
можно скорее, в следующих выпусках; в Angular у них более «долгосрочный» подход, поэтому они работают над тем, чтобы добавить его в версию 2 платформы.
Выводы
Мы провели длительный тур, чтобы рассмотреть плюсы и минусы нескольких вариантов дизайна, и взглянули на будущее веб-разработки. Надеемся, что после прочтения всего поста вы, по крайней мере, знаете о множестве решений и шаблонов, которые могут помочь вам справиться с обработкой событий и связыванием данных. Когда вы сталкиваетесь со своей следующей проблемой проектирования, имейте в виду, что не существует единого правильного решения для всех проблем.