Компания
1 212,20
рейтинг
24 января 2014 в 13:07

Разработка → jQuery Events изнутри

Статья написана в рамках конкурса среди студентов Технопарка Mail.ru.
image

Думаю, JavaSript-библиотека jQuery в представлении не нуждается, но на всякий случай напомню, что jQuery призвана ускорить разработку, предоставить синтаксический “сахар” для нативного js и избавить разработчиков от проблем, связанных с кроссплатформенностью.
Прежде чем говорить о том, как устроена обработка событий в jQuery, нельзя не упомянуть об истории обработки событий в браузере.

Обработка событий на js
Давайте немного вспомним историю возникновения браузеров. Шли уже далекие 90-е; Internet Explorer еще не был самым распространенным браузером, а балом правил Netscape Navigator. Именно разработчики Navigator-а предложили первую модель обработки событий на js (в настоящее время эту модель чаще всего называют DOM Level 0 Event Model).

DOM Level 0 Event Model

Характеризуется данная модель следующими основными факторами:
  • Ссылку на функцию-обработчик записывают непосредственно в свойство dom-объекта. Названия всех событий имеют префикс “on” — onclick, onload и т.п.
  • Все параметры события попадают в обработчик в виде Event Object первым аргументом во всех браузерах, кроме IE. В нем параметры находятся в window.event.
  • События могут подниматься от узла, в котором они возникают, до родителя, потом — до родителя родителя и так далее. Данный процесс обычно называют фазой всплытия.
  • Невозможно установить несколько обработчиков одного события на элементе.

Функцию-обработчик можно присвоить свойству DOM-элемента как в js-скрипте, так и непосредственно в HTML-разметке:
Скрипт HTML
var element = document.getElementById('id');
element.onmousemove = function (e) { /* … */ };
<a onclick="return{'b-link':{}}" >...</a>
<body onresize="onBodyResize()" >...</body>

Стоит отметить, что, хоть данная модель не была стандартизована W3C, но ее поддерживают все современные браузеры (по крайней мере на десктопе). И этой моделью пользуются до сих пор. Примеры (HTML) взяты с yandex.ru и vk.com.
Модель простая, как три копейки, но жизнь не стоит на месте…

DOM Level 2 Event Model

image

В 2000 г. W3C выпустила спецификацию DOM Level 2 Event Model, которую можно охарактеризовать следующим:
  • установка обработчика с помощью метода addEventListener (удаление с помощью removeEventListener);
  • не используется префикс on в названиях событий;
  • Event Object аналогичен DOM Level 0 Event Model;
  • неограниченное количество слушателей одного и того же события на элементе;
  • фаза всплытия из DOM Level 0 Event Model;
  • добавлена фаза захвата, предшествующая всплытию, в которой событие спускается от корневого элемента DOM-дерева вниз до элемента, в котором возникло событие.

Метод регистрации на обработку имеет следующий синтаксис addEventListener(eventType, listener, [useCapture=true]):
  • eventType — тип события (‘click’, ‘change’ и т.д.);
  • listener — ссылка на функцию-обработчик;
  • useCapture — логическая переменная, которая определяет, на какую фазу мы подписываемся (true — захвата, false — всплытия).

Тогда подписка на изменение размера окна браузера имеет вид:
window.addEventListener('resize', function (event) {
  /* … */
});


Internet Explorer Event Model

Разработчики из Microsoft всегда шли своим путем и до IE 9 версии не поддерживали общепринятую модель событий, но у них была своя, с блэкджеком atachEvent и detachEvent.
Данная модель схожа с DOM Level 2 Event Model, но имеет ряд отличий (существует и множество других, но эти — самые основные):
  • методы attachEvent и detachEvent для установки и удаления обработчиков соответственно;
  • префикс ‘on’ в названиях событий;
  • отсутствие фазы захвата.


Итог

Учитывать различия между браузерами мучительно больно, но не нужно! Не забываем, что со всеми этими проблемами столкнулись до нас — в частности, поэтому и появилась библиотека jQuery.

Обработка событий с помощью jQuery


Здесь и далее под jQuery мы будем понимать jQuery 1.10.2, актуальную на данный момент версию из ветки 1.0.
Когда мы используем jQuery, можно смело забыть о различиях между addEventListener и attachEvent и о многом другом, потому что библиотека предоставляет для разработчика следующее:
  • унифицированный способ регистрации обработчиков событий (с помощью методов);
  • неограниченное количество обработчиков одного и того же события на одном элементе;
  • передача нормализованного Event Object в обработчик.

Итак, в jQuery существует множество методов, с помощью которых можно подписаться на обработку событий:
  • bind — уставливает обработчик непосредственно на элемент(ы). В качестве аргументов принимает название события и callback;
  • click, blur, scroll и множество других shortcut-методов аналогичны вызову bind, только типом события является само название метода;
  • on — главный метод, позволяет как привязать обработчик непосредственно к элементу, так и делегировать обработку событий; для делегирования необходимо передать необязательный параметр selector;
  • delegate — alias для метода on с измененным набором аргументов;
  • one — тоже, что и метод on, но обработчик сработает только при первом возникновении события.

Способов отписаться от события три: unbind (для bind, click и подобных), undelegate и off.
Но…
image

jQuery не только является для нас уровнем абстракции от addEventListener и attachEvent, и не только нормализует Event Object.
Под капотом у jQuery спрятан обширный пласт кода, состоящего из:
  • шаблона проектирования Observer, реализующего логику централизованной установки/удаления обработчиков событий;
  • системы хуков и фильтрации параметров для Event Object;
  • а так же возможности расширения функционала jQuery.event с помощью Special Event API.

Обо всем по порядку.

jQuery.event


Уже многие разбирали, как устроена jQuery изнутри (например, тут), но почему-то механизм обработки событий обходили стороной. Так давайте же восполним этот пробел.
Обработка событий с точки зрения пользователя библиотеки проходит через три этапа:
  1. установка обработчика события — вызов метода bind и т.п.
  2. обработка события — вся та магия, которую предварительно осуществляет jQuery до момента, когда передает нормализованный Event Object в “наш” обработчик;
  3. удаление обработчика событий — вызов метода unbind и т.п.


Special Event API

Библиотека jQuery предоставляет стороннему коду возможность влиять на процесс обработки событий. В недрах библиотеки находится объект jQuery.event.special; его ключами являются названия событий, в обработку которых мы хотим вмешаться, а значением может быть объект со следующими свойствами:
  • noBubble — флаг, определяющий, должно ли всплывать событие при вызове метода trigger. По умолчанию — false (событие всплывает). В самой библиотеке используется для события load, чтобы событие load на картинках не всплывало до window.
  • bindType, delegateType (striing) позволяет поменять тип обрабатываемого события. Например, с помощью этого в jQuery реализованы события mouseenter/mouseleave через стандартные mouseover/mouseout.
  • setup — функция вызывается, когда обработчик данного типа устанавливается в первый раз на элементе.
  • teardown вызывается, когда удаляется последний обработчик данного типа с элемента.
  • add — функция вызывается всякий раз, когда добавляется обработчик.
  • remove вызывается всякий раз, когда удаляется обработчик.
  • handle будет вызываться всякий раз, когда возникает событие вместо обработчика, переданного в метод on (или bind, one и т.п.). Использование этого special-метода хорошо представлено на официальной странице.

Возможно как вмешиваться в механизм обработки существующих событий, так и создавать свои. Вот так можно создать событие pushy, которое на самом деле будет реагировать на стандартный click:
jQuery.event.special.pushy = {
   bindType: "click",
   delegateType: "click"
};

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

Установка обработчика

image

Давайте представим, что мы исполняем код
 $('div').click(function (e) { /* ... */ });
вместе с jQuery. Разберем, что же происходит на каждом этапе.
External handler. Для того чтобы обработать событие, необходимо создать обработчик, получить на него ссылку и передать ее в любой метод jQuery, отвечающий за установку обработчика. Что мы собственно и сделали, создав функцию с помощью fucntion (e) { /* … */ } и вызвав метод click с ней в качестве параметра.
Методы bind, one, delegate и т.п. Наш обработчик попадает в один из этих методов, внутри которых с различным набором параметров на самом деле метод on.
Метод on состоит из солидного набора if-else блоков — происходит переработка и упорядочивание всех возможных вариантов параметров, с которыми он может быть вызван. Также именно в методе on реализуется логика установки единожды срабатывающего обработчика (метод one). В завершение вызывается метод each для текущего объекта jQuery.
Метод each — часть jQuery core, именно с помощью этого метода библиотека итерируется по “набору jQuery” (смотреть тут). Метод each умеет обходить массивы и объекты в зависимости от предоставляемого интерфейса. Если объект предоставляет свойство length, то итерация происходит так же, как по массиву, что является отличным примером микрооптимизации. Итак, для каждого DOM-элемента из набора jQuery вызывается метод add.
Вся последующая обработка происходит в методе add (из объекта jQuery.event). В начале каждому external handler назначается уникальный идентификатор. Для этого (и не только) в jQuery есть глобальный счетчик jQuery.guid, который при каждом использовании инкрементируется.
Затем создается функция, которую здесь и далее мы будем называть main handler, принимающая на вход Event Object и вызывающая метод dispatch.
Если обработчик данного события устанавливается впервые для этого элемента, то создается очередь обработчиков (в нее будут записываться все external handler-ы) и вызывается special.setup, если он задан для данного типа события.
Внутри special.setup разработчик может реализовать свою логику создания main handler и подписки на событие посредством функций addEventListener или attachEvent, или какую-то другую независимую логику.
Далее производится подписка созданного main handler-а на нужное событие с помощью методов addEventListener или atachEvent в зависимости от браузера, но это происходит только в том случае, если special.setup не задан или вернул false.
Затем управление передается в special.add, если он задан. В отличие от special.setup, special.add выполняется всякий раз, когда производится установка обработчика через jQuery.
И уже после всего этого переданный в самом верху external handler попадает в очередь обработчиков (link) и будет вызван, когда возникнет событие. Об этом далее.

Обработка события

image

event occurs — событие возникает в DOM-элементе и попадает в созданный библиотекой main handler (он подписан на событие с помощью addEventListener или attachEvent), в котором вызывается метод dispatch.
В метод dispatch в качестве параметра event попадет оригинальный ненормализованный Event Object, который передается в метод fix для нормализации.
Внутри метода fix проверяется, не готов ли Event Object (не производилась ли нормализация до этого), и если нет, то:
  • Проверяется, есть ли fixHooks[type], где type — тип возникшего события. fixHooks[type] — это объект, у которого могут быть два свойства:
    • props — массив названий свойств, который необходимо скопировать из оригинального Event Object в нормализованный;
    • filter — функция, нормализующая (преобразующая) параметры события.
  • Если fixHooks определенного type есть, то используется этот объект; если нет, то с помощью специальных regexp проверяется, является ли наше событие key-event или mouse-event (каждый из этих типов имеет собственный fixHooks — keyHooks и mouseHooks соответственно).
  • Затем создается “пустой” нормализованный Event Object (с помощью new jQuery.Event), и все свойства, названия которых присутствуют в массивах jQuery.event.props и fixHooks.props, копируются из оригинального Event Object в нормализованный. Под конец работы метода fix вызывается функция filter, если она определена, и нормализованный Event Object возвращается обратно в dispatch.

Затем вызывается special.preDispatch, в зависимости от его результата может завершиться дальнейшая обработка события (если preDispatch вернет false).
После этого для каждого обработчика из очереди, которая была создана на этапе установки, вызывается special.handler, если он есть. Если его нет, то обработчик вызывается непосредственно external handler (link).
В конце метода dispatch после срабатывания всех обработчиков вызывается special.postDispatch. Метод special.postDispatch также можно определить в своем коде, как и другие special методы.

Удаление обработчика

Удаление обработчика проходит стадии, похожие на стадии из установки, а именно: удаление, начинающееся, например, с unbind, так или иначе попадает в метод off, потом jQuery итерируется по набору с помощью each, и в завершении вызывается уже не метод add, а метод remove (спасибо, Кэп).
В методе remove (из jQuery.event) совершаются обратные по отношению к методу add операции:
  • Выполняется поиск external handler в очереди обработчиков по идентификатору. Затем external handler удаляется из очереди.
  • Eсли очередь опустела, то удаляется и она, а также main handler c помощью jQuery.removeEvent, что, в свою очередь, также является оберткой над removeEventListener и detachEvent.

На процесс удаления обработчика можно повлиять, определив функции special.remove и special.teardown. remove вызывается всякий раз, а teardown тогда, когда очередь пуста.

Итог


Мы вспомнили, как обрабатывались события в браузере, что произошло с клиентским js-кодом при выходе на арену jQuery, и что происходит внутри этой библиотеки. Единый main handler для элемента, нормализация Event Object через копирование и фильтрацию, очередь обработчиков события и зоопарк методов установки — вот такую реализацию Observer pattern нам подарили создатели jQuery.
За бортом остались как минимум такие важные для Event-ов темы, как делегирование, preventDefault(), stopPropagation(), trigger и namespaces. Но рассказать все могут только исходники. Так что github ждет. :)
Автор: @arusakov

Комментарии (2)

  • +4
    Родной fix(), оказывается, не очень быстрый. Тут описано, почему. Используем этот фикс у себя в проекте — разница в скорости обработки — 4-кратная.
    • 0
      Кстати, это хорошая тема для другой статьи. Ведь в этой описан только принцип работы, а плюсы и минусы или альтернативные варианты реализации не озвучены.

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

Самое читаемое Разработка