Гибкое управление событиями в jQuery — плагин jquery-behavior

    Привет, Хабр!

    Меня зовут Вячеслав Гримальский, я работаю над конструктором посадочных страниц, в котором страница собирается перетаскиванием блоков.

    Я расскажу об инструменте для работы с событиями, который изначально являлся частью конструктора, но затем был вынесен в отдельный плагин для jQuery — jquery-behavior.

    Плагин использует функционал jQuery, дополняя его следующими возможностями:

    • Работа с раздельными группами обработчиков событий. Для этого используются контроллеры событий.
    • Можно отключить все обработчики событий контроллера разом, не перечисляя их.
    • Слежение за срабатываниями обработчиков событий.
    • Можно узнать точное количество вызовов каждого из них.
    • Можно приостанавливать работу отдельных обработчиков событий, или всего контроллера разом.
    • Возможность получить полный перечень обработчиков событий конкретного элемента, обработчиков событий с определенным пространством имен или просто всех обработчиков событий контроллера.

    Покажу сразу, о чем идет речь:

    // Создаем контроллер событий.
    // Каждый контроллер работает со своей группой событий, и не знает о других контроллерах.
    var behavior = $.Behavior();
    
    // Добавляем обработчики событий. Синтаксис функций такой же, как в jQuery.
    behavior('body').click(function () {});
    behavior(window).on('resize.demo', function () {});
    behavior('.top').on('click.demo', '.btn', function () {});
    
    // Приостанавливаем выполнение обработчиков событий, подходящих фильтру.
    behavior.pause({
        types: '.demo'
    });
    
    // Возобновляем выполнение обработчиков событий, подходящих фильтру.
    behavior.resume({
        target: 'span',
        types: 'click.demo'
    });
    
    // Отключаем все обработчики событий, созданные контроллером.
    behavior.off();
    

    Контроллеры событий


    var behavior = $.Behavior();
    

    Контроллер событий — объект, который может добавлять обработчики событий, и который хранит о них всю информацию.

    Он может удалять и приостанавливать обработчики событий, но только те, который в нем же и были добавлены.

    Это позволяет разделять обработчики событий разных, независимых, частей проекта, и работать с ними отдельно.

    Работа с обертками в стиле jQuery


    Плагин, позволяет создавать обертки объектов для работы с ними.

    jQuery:
    $('body').click(function () {});
    $(window).on('resize', function () {});
    

    Плагин:
    behavior('body').click(function () {});
    behavior(window).on('resize', function () {});
    

    Здесь все функции и их синтаксис скопированы из jQuery, полный их список:
    on one trigger triggerHandler off bind unbind delegate undelegate hover blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu

    Вы можете просто взять работающий с событиями код на jQuery и заменить функцию "$" на «behavior», или как у вас будет называться контроллер событий, и все будет работать.

    jQuery:
    $('body').click(function () {}).one('mousemove', function () {}).trigger('click');
    

    Плагин:
    behavior('body').click(function () {}).one('mousemove', function () {}).trigger('click');
    

    При этом все функции, создаваемые таким образом будут закреплены за используемым контроллером, и вы сможете работать с ними, используя все остальные функции контроллера.

    Добавление обработчиков событий — функции behavior.on() и behavior.one()


    По аналогии с функциями jQuery, on добавляет обработчик события, а one добавляет обработчик события, который выполняется всего один раз.

    Синтаксис такой:

    behavior.on({
        // Стандартные параметры
        target: 'body', // Элемент, на которые вешается обработчик
        types: 'click.namespace', // Название или названия события с пространствами имен
        selector: '.btn', // Не обязательно. Селектор для делегирования событий
        handler: function (event) {}, // Обработчик события
    
        // Дополнительные параметры
        throttle: { // Не обязательно. Создает обертку обработчика функцией _.throttle
            wait: 1000,
            leading: true,
            trailing: true
        },
        after: 1, // Не обязательно. Создает обертку обработчика функцией _.after
        log: true // Не обязательно. Позволяет "заглушить" логи событий, о которых позже.
    });
    

    Несколько примеров.

    behavior.on({
        target: window,
        types: 'resize',
        handler: handlerFn
    });
    // аналогично
    behavior(window).on('resize', handlerFn);
    

    behavior.on({
        target: window,
        types: 'resize',
        handler: handlerFn,
        throttle: {
            wait: 200,
            leading: false
        }
    });
    // аналогично
    behavior(window).on('resize', _.throttle(handlerFn, 200, { leading: false }));
    

    behavior.on({
        target: 'body',
        types: 'click',
        selector: '.btn',
        handler: handlerFn,
        after: 2
    });
    // аналогично
    behavior('body').on('click', '.btn', _.after(2, handlerFn));
    

    При использовании «полной» записи с параметрами after и throttle наличие библиотеки underscore или lodash не обязательно, поскольку эти функции встроены в плагин.

    Отключение событий — функция behavior.off()


    Вы можете отключить одним вызовом все обработчики событий, созданные контроллером.

    behavior.off();
    

    Вы можете отключить все обработчики событий определенного элемента или группы элементов:

    behavior.off({
        target: window
    });
    // аналогично
    behavior(window).off();
    

    behavior.off({
        target: $btns
    });
    // аналогично
    behavior($btns).off();
    

    behavior.off({
        target: 'body .btn'
    });
    // аналогично
    behavior('body .btn').off();
    

    Вы можете отключить все обработчики событий определенного типа и пространства имен:

    behavior.off({
        types: 'click'
    });
    

    behavior.off({
        types: 'click.namespace'
    });
    

    behavior.off({
        types: '.namespace1, .namespace2'
    });
    

    behavior.off({
        types: 'click.namespace1, .namespace2'
    });
    

    Вы можете удалять и делегированные события.

    behavior.off({
        target: 'body',
        types: 'click',
        selector: '.btn'
    });
    // аналогично
    behavior('body').off('click', '.btn');
    

    behavior.off({
        target: 'body',
        types: '.namespace',
        selector: '**'
    });
    // аналогично
    behavior('body').off('.namespace', '**');
    

    А так же удалить все обработчики по ссылке.

    behavior.off({
        handler: handlerFunction
    });
    

    И конечно, все это вы можете комбинировать.

    behavior.off({
        target: 'body',
        types: '.namespace',
        handler: handlerFunction1
    });
    // аналогично
    behavior('body').off('.namespace', handlerFunction1);
    

    behavior.off({
        target: 'body',
        handler: handlerFunction1
    });
    

    Приостановление и возобновление работы обработчиков — функции behavior.pause() и behavior.resume()


    Аргументы такие же, как и у функции behavior.off().

    Приостановить одним вызовом все обработчики событий, созданные контроллером.

    behavior.pause();
    

    И вернуть их в работу:

    behavior.resume();
    

    И далее все примеры по аналогии с behavior.off().

    behavior.pause({
        target: window
    });
    

    behavior.pause({
        types: 'click.namespace1, .namespace2'
    });
    

    behavior.pause({
        target: 'body',
        types: 'click',
        selector: '.btn'
    });
    

    behavior.pause({
        target: 'body',
        types: '.namespace',
        handler: handlerFunction1
    });
    

    Приостановление и возобновление работы контроллера — функции behavior.start() и behavior.stop()


    Эти функции похожи на behavior.pause() и behavior.resume(), но имеют некоторые отличия.

    Они работают не на уровне конкретных обработчиков событий, а на уровне контроллера, то есть останавливают не обработчики событий, а сам контроллер. Поскольку останавливается весь контроллер, то обработчики событий, добавленные после выполнения behavior.stop() работать не будут до тех пор, пока мы не возобновим работу контроллера функцией behavior.start().

    При создании контроллера так же вызывается behavior.start(), а при его разрушении — behavior.stop().

    События контроллера


    При создании контроллера можно указать параметры onStart, onStop и onFire.

    var behavior = $.Behavior({
        onStart: function (data) {},
        onStop: function (data) {},
        onFire: function (event) {}
    });
    

    Функция onFire, вызывается при каждом срабатывании любого из обработчиков событий контроллера. Аргументы и контекст получает те же, что и обработчик события.

    Функция onStart выполняется при вызове behavior.start(). Может принимать первый аргумент.

    Функция onStop, что логично, выполняется при вызове behavior.stop(), и так же может принимать первый его аргумент.

    Чтобы создаваемый контроллер событий изначально был выключен, onStop не вызывался, а события не выполнялись, пока вы не вызовете behavior.start(), нужно при создании контроллера указать флаг active со значением false:

    var behavior = $.Behavior({
        active: false
    });
    

    Разрушение контроллера — behavior.destroy()


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

    Использование нескольких контроллеров


    Вы можете использовать один контроллер на весь проект, а можете эффективно делить события на разные контроллеры.

    Хороший пример — реализация Drag'n'Drop с помощью плагина.

    Один контроллер — события «спящего» состояния, другой — события перетаскивания, и мы между ними просто переключаемся.



    Демо на JSFiddle: http://jsfiddle.net/fm22ptxv/
    Большие красные пиксели перетаскиваются мышкой.

    Инструменты для отладки


    В плагин встроены инструменты для отладки, позволяющие понять, что происходит с обработчиками. Какие из них до сих пор активны, какие вызываются, а какие бездействуют, и сколько их вообще «в живых» осталось.

    Получение информации о контроллере — behavior.data()


    Возвращает всю необходимую информацию о контроллере — текущее состояние, список обработчиков событий.

    {
        log: false,
        namespace: "bhvr",
        onFire: null,
        onStart: null,
        onStop: null,
        records: Array
    }
    

    Важно понять, что из себя представляют объекты из массива records.

    Допустим, у нас есть 2 кнопки с классами .btn1 и .btn2, и мы хотим назначить им по 2 одинаковых обработчика события:

    behavior('.btn1, .btn2').on('mousedown mouseup', handler);
    

    На самом деле будет добавлено 4 «низкоуровневых» обработчика событий, как если бы мы писали так:

    behavior('.btn1').on('mousedown', handler);
    behavior('.btn1').on('mouseup', handler);
    behavior('.btn2').on('mousedown', handler);
    behavior('.btn2').on('mouseup', handler);
    

    Так вот, поле records будет содержать информацию о обработчиках в таком виде, как мы их обьявляем.

    В нашем примере это:

    {
        targets: JQuery[], // содержит $('.btn1, .btn2')
        types: 'mousedown mouseup', // названия событий и пространства имен
        handler: function (event) {}, // обработчик события
        selector: undefined, // селектор для делегирования событий
        calls: 0, // количество вызовов обработчика handler
        bindings: Array // те самые "низкоуровневые обработчики"
    }
    

    А уже в bindings из records будет 4 подобных объекта:

    {
        target: JQuery[], // содержит $('.btn1')
        type: 'mouseup', // название события
        namespaces: string[], // список пространств имен
        handler: function (event) {}, // обработчик события
        selector: undefined, // селектор для делегирования событий
        calls: 0, // количество вызовов обработчика handler
        paused: false // состояние обработчика
    }
    

    Количество вызовов (calls) объекта в records всегда будет равняться сумме количеств вызовов всех его bindings.

    А в bindings они различны, и считаются отдельно, что удобно, когда нужно узнать, какие конкретно обработчики событий вызываются, и на каких элементах они срабатывают.

    Получение списка обработчиков событий — функция behavior.filter()


    Выполняет поиск по всем «низкоуровневым» обработчикам, и возвращает их список.

    Синтаксис у функции такой же, как и у behavior.off(), behavior.pause() и behavior.resume().

    Получение полного списка обработчиков.

    behavior.filter();
    

    Получение обработчиков событий объекта:

    behavior.filter({
        target: window
    });
    

    И так далее.

    behavior.filter({
        types: 'click.namespace1, .namespace2'
    });
    

    behavior.filter({
        target: 'body',
        types: 'click',
        selector: '.btn'
    });
    

    behavior.filter({
        target: 'body',
        types: '.namespace',
        handler: handlerFunction1
    });
    

    Логи событий


    Иногда удобно бывает отслеживать, какое событие когда выполняется.

    Контроллер имет встроенный механизм логов, который выводит в консоль информацию о каждом выполнении обработчика событий, а так же об остановке и продолжении работы самого контроллера (функции behavior.stop() и behavior.start()).

    Чтобы контроллер событий выводил в консоль информацию обо всем, что происходит, достаточно при создании контроллера указать флаг log со значением true:

    var behavior = $.Behavior({
        log: true
    });
    

    Так же вы можете включить логи уже после создания контроллера:

    behavior.logOn();
    

    Выключить логи можно так:

    behavior.logOff();
    

    Вы можете назначить свой обработчик логов, который вместо вывода сообщений в консоль будет делать то, что нужно вам.

    Для этого при создании контроллера нужно указать функцию logFn:

    var behavior = $.Behavior({
        logFn: function (type, behavior, event, data) {}
    });
    

    Аргумент type содержит тип лога — start, stop или fire. Аргумент behavior содержит ссылку на сам контроллер. Дальше идут те аргументы, которые передаются в обработчик события, это event и data. Аргумент data может отсутствовать.

    Тестирование


    Для того, чтобы протестировать работу плагина, были взяты родные тесты событий jQuery, и слегка адаптированы под себя.

    При этом в тестах были обнаружены несколько обработчиков событий, которые не удалялись, хотя за этим в тестах пытаются следить. Мой коммит с исправлением этого был принят в jQuery.

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

    Заключение


    Первая версия плагина была написана около полутора года назад. С тех пор плагин многократно переписывался и дорабатывался, пока не принял свой текущий вид.

    Все это время он активно используется мной в разработке, и он на самом деле очень удобен.

    Буду рад любым комментариям, замечаниям и предложениям.

    Ссылка на гитхаб: https://github.com/grimalschi/jquery-behavior

    Большое спасибо!
    • +18
    • 18,5k
    • 6
    Поделиться публикацией
    Комментарии 6
      +3
      Как то не очень понятно, чего не хватило в обычных кастомных событиях jQuery с использованием неймспейсов.

      Чем

      behavior('.btn1, .btn2').on('mousedown mouseup', handler);
      


      Лучше чем
      $('.btn1, .btn2').on('mousdown.myns mouseup.myns', handler);
      


        +1
        Гораздо удобнее навешивать события на корневой элемент вашего виджета, тогда и плагина не надо:

        var $root = $(domSelector)
        
        $root.on('mousedown.ns mouseup.ns', '.btn', function (e) {
        
        })
        
        $root.on('.ns', myLog)  // включить лог
        
        $root.off('.ns', myLog)  // выключить лог
        
        $root.off('.ns)  // выключить все обработчики
        
        
          +2
          Вся прелесть в том, чтобы не заботиться о неймспейсах, о том, что все события нужно вешать на один корневой элемент, а потом, когда нужно все отключить — перечислять все элементы с событиями.

          А если вам потребуется «заглушить» выполнение события, то внутри обработчика вам нужно ставить условие, еще потребуется внешний флаг, глядя на который обработчик будет понимать, выполняться ему, или нет.

          А если у вас 2 состояния, как например, при перетаскивании — состояние ожидания и состояние перетаскивания, то у вас встает уже вопрос с управлением событий этих двух состояний. Опять нужно будет делать внешние флаги, по которым обработчики будут понимать, должны они выполняться, или нет.

          Исходя из всех этих мелочей, и многих других, было решено писать плагин, который бы облегчил всю эту рутину.

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

          Кстати, в вашем примере "$root.on('.ns', myLog)" работать не будет, поскольку неймспейс указан, а событие нет. Отключать события по неймспейсу можно, а слушать нельзя.
        +1
        Странно, но у меня демо в яндекс браузере не работает… — jsfiddle.net/fm22ptxv/
        В консоли ошибки:
        1) Refused to execute script from 'https://raw.githubusercontent.com/grimalschi/jquery-behavior/master/jquery.behavior.js' because its MIME type ('text/plain') is not executable, and strict MIME type checking is enabled. fiddle.jshell.net/:1

        2) Uncaught TypeError: undefined is not a function (index):32

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

      Самое читаемое