В этой заметке расскажу, как я использую пользовательские события jQuery (custom events) в своей работе.
Имитиация событий
Дана простая задача, реализацию которой наблюдают всее: когда пользователь достаточно прокрутил страницу вниз, анимированно отображается блок-врезка «Лучшее за 24 часа», и скрывается, когда пользователь прокручивает страницу вверх. Эта задача решается подвешиванием обработчика на события scroll и resize окна (window), который занят двумя вещами: вычисляет, нужно или нет отображать/скрывать блок-врезку, и в зависимости от результата производит анимацию отображения или осуществляет скрытие.
Если эта задача стояла перед нами, как бы мы приступили к ее реализации? Ну, например, написали такой кусочек кода:
$(window).on('scroll resize', function(){
// берем координаты окна, решаем, нужно ли отображать/скрывать
// анимируем позиционирование отображения (или скрываем)
});
Сразу отмечу, что с версии 1.7 время .bind()
, .delegate()
, .live()
закончилось: отныне мы используем универсальный метод подключения обработчиков событий .on()
(за исключением разве что случаев, когда нам нужно применить единичное «подслушивание» .one()
, а также поймать dom-ready при помощи $(function(){ })
).
В примере выше мы ловим события прокрутки окна и изменения его размеров, используя в качестве обработчика анонимную функцию.
Ничего не забыли? Да вроде нет. Проверяем. При наступлении события прокрутки окна отрабатывается тело функции-обработчика: взять размеры видимого прямоугольника, посчитать координаты и подвинуть к ним div-ку. Стойте. А если пользователь не будет ничего «крутить», мы наш элемент так и не спозиционируем в самом начале работы? Ну да. Похоже, все-таки забыли. Хорошо, напишем так:
$(window).on('scroll resize', function(){
// get new coordinates
// animate repositioning
});
// get new coordinates
// animate repositioning
Беда: получился код «с душком». Keep calm и избавляемся от копипаста:
$(window).on('scroll resize', repositionAnchor);
function repositionAnchor(){
// get new coordinates
// animate repositioning
}
repositionAnchor();
Не знаю как вам, а так мне еще меньше нравится.
Кто-то скажет: «вернись к первоначальному примеру и подвяжись ещё на событие 'load'». Не вопрос, подвяжусь. А что, если этот код в силу каких-то причин исполняется уже после прохождения события load?
Решение на поверхности. Благодаря jQuery мы можем самостоятельно имитировать прохождение событий, которых ожидаем, при помощи метода .trigger()
. Наш первоначальный пример становится таким:
$(window).on('load scroll resize', function(){
// get new coordinates
// animate repositioning
}).trigger('scroll');
Навесились на load, scroll и resize, и не дожидаясь, пока любое произойдет, имитируем наступление одного из них. Profit. И декларация, и инициализация.
Пользовательские события
Ну, а что, если сам документ укоротился или как-то изменил форму вследствие отрисовки новых данных, полученных асинхронно? Необходимость отобразить блок у нас появится, а события 'scroll' так и не произойдет? Тогда… когда станет нужно, мы «позовем» $(window).trigger('scroll')
— куда уж проще.
Проще то — да, но а то, что «scroll» — означает «прокрутку», а не «изменение внутренностей документа», это ничего? Лично мне — чего. Эх, было бы у объекта window
событие 'change'
или 'redraw'
, скажем. Тогда можно было бы навеситься на него, а в последствии — «выстреливать» им, когда время придет.
А что если я вам скажу, что нам ничего не мешает сделать это? Что неважно, есть ли такое событие у объекта или нет? Вот именно так:
$(window).on('load scroll resize redraw', function(){
// get new coordinates
// animate repositioning
}).trigger('scroll');
// some time later, just when we need it:
$(window).trigger('redraw'); // << custom event
Стандартного события с названием redraw
не существует. Мы сами его сейчас придумали. Поэтому оно называется «пользовательским».
В jQuery, мы можем «навешивать» прослушивание такого события на все элементы, кроме текстовых узлов и узлов с комментариями.
Итак, смотрите, получается, что мы не ограничены жестким списком событий взаимодействия пользователя с DOM: мы можем придумывать свои собственные названия событиям, «выстреливать» ими, и, конечно же, навешивать их обработчики. Такая свобода действий дает нам возможность подняться на уровень выше в определении блоков (модулей) приложения и обеспечить их взаимодействие на принципах свободного связывания (loose coupling).
Конечно, об этом мы уже давно знали из самой документации jQuery API: событие может иметь любое имя, или тип, например, 'onDataAvailable'
, 'elvisHasLeftTheBuilding'
и 'the_answer_to_life_the_universe_and_everything_is_ready'
. Многие знали это, но, уверен, далеко не все пользовались.
Два слова про пространства имен событий (event namespaces). Все, что идет в названии события через точку, является так называемым пространством имен этого события ('onDataAvailable.widgetOne'
— «widgetOne» — пространство имен для данного события). Их может быть несколько ('onDataAvailable.widgetOne.dataEvents'
— тут задействовано два пространства имен: widgetOne и dataEvents). Это — удобный способ группировать события. Но пространства имен заслуживают отдельной статьи, поэтому здесь о них больше ни слова. Один вывод из сказанного: мы избегаем названий событий с точками.
К чему прицепиться?
Мы только что заново узнали, что можно «выстреливать» и «слушать» события с абсолютно любыми названиями. Однако на первый план, выходит другой вопрос. Если для слушания нажатия ('click'
), мы навешиваемся на DOM-элемент <a href="..">..</a>
(или на один из его родителей, который получит нужное событие при его всплытии), то на что же нам навешиваться, когда мы хотим послушать наступление пользовательского события? Для примера, события 'onDataAvailable'
, которое должно наступить, по нашему замыслу, когда важные для приложения данные подгрузились и прошли обработку?
Я для себя вначале на этот вопрос ответил так:
$(window).on('onDataAvailable', function(){
// логика
});
Но сразу отказался от этого рабочего варианта. Мне показалось неправильным использовать самый главный объект клиентской среды в качестве шлюза для «хождения» пользовательских событий. Одна из причин: мало ли, по недогляду или как, одно из событий приобретет название известных событий ('load'
, 'resize'
и т.п.), и сработают не те «слушатели».
Тогда я создал, не привязывая к документу, пустой элемент (var eventNode = $('<div></div>')
) и подключал к нему слушателей — eventNode.on('customEvent', function(){ /*...*/ })
или «выстреливал» из него событием: eventNode.trigger('customEvent')
.
Но мне все равно продолжало казаться неправильным, что создавать пользовательские события мы можем, а отсылать или слушать их из не-DOM-элементов — нет. И вот, перечитав еще раз документацию и покопавшись в исходниках jQuery, я пришел к выводу, что eventNode из нашего примера вполне может быть простым объектом ({}
), обернутым в $()
, вот так: var eventNode = $({})
. Такой объект не только имеет у себя в прототипе методы .on()
, .trigger()
, .one()
, но и реально работает с ними.
Вот, упрощенно, что получилось:
var events = (function(){
var eventNode = $({});
return {
on: on,
trigger: trigger
};
function on(){
eventNode.on.apply(eventNode, arguments);
}
function trigger(){
eventNode.trigger.apply(eventNode, arguments);
}
})();
// events.on('customEvent', function() {});
// events.trigger('customEvent');
Что сказать?
Я собираюсь устранить упущение, присутствовашее во всех вышеприведенных примерах. Наши события не передают никаких данных.
Устраняется это упущение легко: вторым параметром в метод .trigger()
, сразу после названия события, мы можем передать собственные данные, которые обработчик получит вторым параметром (первый, как вы знаете, — объект самого события). Мне даже не придется переписывать недавний пример:
events.on('onDataAvailable', function(evt, data) {
var items = data.items,
page = data.page,
total = data.total;
// render items based on data
});
events.trigger('onDataAvailable', {
items: [ /*... */ ],
page: 3,
total: 10
});
На этом я прервусь. Во второй части я расскажу о попытке типизации событий, фильтрации на основании шаблонов ожидаемых данных, а также о разрешении проблемы закольцованности вызовов.
Спасибо за внимание!
Автор: andrevinsky