Пользовательские события в действии

в 5:54, , рубрики: Events, javascript, jquery, workflow, метки: , ,

В этой заметке расскажу, как я использую пользовательские события 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js