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

    В этой заметке расскажу, как я использую пользовательские события 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-элемент .. (или на один из его родителей, который получит нужное событие при его всплытии), то на что же нам навешиваться, когда мы хотим послушать наступление пользовательского события? Для примера, события 'onDataAvailable', которое должно наступить, по нашему замыслу, когда важные для приложения данные подгрузились и прошли обработку?

    Я для себя вначале на этот вопрос ответил так:

    $(window).on('onDataAvailable', function(){
    // логика
    });
    


    Но сразу отказался от этого рабочего варианта. Мне показалось неправильным использовать самый главный объект клиентской среды в качестве шлюза для «хождения» пользовательских событий. Одна из причин: мало ли, по недогляду или как, одно из событий приобретет название известных событий ('load', 'resize' и т.п.), и сработают не те «слушатели».

    Тогда я создал, не привязывая к документу, пустой элемент (var eventNode = $('')) и подключал к нему слушателей — 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
    });
    
    


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

    Спасибо за внимание!
    • +20
    • 18.8k
    • 6
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 6

      +1
      А вы уверены, что вам действительно это нужно?
      События реализуют решение в первую очередь для двух задач: всплытие (важно для интерфейсов) и независимость слушателей и эмиттера (важно для автономных блоков).

      Половина примеров в тексте — то, что является фактически методом (тот же .trigger('redraw')), вторая может быть реализована в 30 строк кода (если отделять от работы с дом-деревом).

      Хотя сам материал глубокий, не спорю, да.
        0
        Извините, ошибся кнопкой. Ответил отдельным комментарием.
        +2
        Спасибо за Ваш отзыв.

        В действительности, толковых примеров использования событийной коммуникации между модулями я и не привел. Если не считать самого последнего кусочка кода. (Первые три секции лишь демонстрировали возможности .trigger() и .on() по отношению к пользовательским событиям.)

        Поэтому полностью с Вами согласен, не надо пользоваться событиями, если мы имеем в виду методы. «Источник известен + получатель известен => позови метод.»
        Другое дело, когда чего-то из этого становится больше и/или исчезает необходимость жесткого соединения. Это, например, когда источник перестает заботиться о том, сколько получателей примет его сообщение и примет ли вообще. Главное — уведомить. А послушать — это на совести слушащих.

        Пример? Я его отложил для второй части, но почему бы и нет. На сайте магазина есть страницы, различающиеся набором и видом виджетов, следящих за изменением корзины. Одни — содержат список товаров, другие — отображают линку с маркером количества товаров в корзине, третьи — предлагают похожие товары на основании уже купленных. Кроме того, добавить товар в корзину пользователь может из различных виджетов (которые могу присутствовать на одних и отсутствовать на других страницах). Но — пользуясь событиями — мы можем сделать так, что успешное добавление товара в корзину (изменение корзины) из любого виджета вызовет событие: "корзина изменена". И может быть — пошлет дополнительные данные. Все виджеты, которые присутствуют в данный момент на странице, заинтересованные в данном событии — предпримут какие-то действия: перерисуются, обратятся за доп. данными и т.п. Так, замигает иконка корзины, пополнится список «Обратите внимание и на эти товары», уйдет пинг на сервер «Подозрительная покупка, сообщите в органы» и т.п.

        Исключительно методами такое делать можно, разумеется; но событийная система как нельзя лучше отвечает потребностям программиста в данном примере — да и поддерживать легче. (Про поддержку событий я хотел написать отдельно в следующей части.)

        Еще раз спасибо!
          +1
          Для этого все же подходит дата-биндинг, rivets например. В данной ситуации есть модель, отвечающая за хранение данных, и есть автономные вьюхи, отвечающие за представление, и при изначальной загрузке модуля он не должен ждать событий: он должен запросить данные с модели, и если есть — сразу отобразить их.
          +1
          Неплохой способ быстро реализовать Pub/Sub шаблон, но идея далеко не нова
            +1
            К концу понял что автор реализовал Pub/Sub =)
            (function( $, window, document ) {
            var o = $( {} );
            $.each({
                trigger: 'publish',
                on:         'subscribe',
                off:         'unsubscribe',
                one:       'subscribeOnce'
            }, function( key, val ){
                $[val] = function() {
                    o[key].apply( o, arguments );
                }
            });
            })( jQuery, window, document, undefined );
            

          Only users with full accounts can post comments. Log in, please.