пятница, 4 октября 2013 г.

Создание собственных событий в JavaScript

Иногда требуется организовать бизнес-логику, основываясь на событийном механизме (шаблон Observer). Обычно это делается разово, но последнее время такие ситуации стали появляться всё чаще, и я решил создать универсальное решение, которое позволит расширять или создавать список событий для любого JavaScript-объекта.

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

//Сначала добавляем нужные методы к объекту
var x = {};
setAddEvent(x);

//Затем добавляем событие. Называем его 'event1'. На выходе получаем функцию, которую нужно будет вызывать, когда событие произойдёт
var f = x.addEvent('event1');

// Теперь добавляем обработчики события - способ стандартный (такой же, как принятый в DOM)
var y = addEventListener('event1', function(){ console.log('Событие 1 произошло!'); }),
    z = addEventListener('event1', function(){ console.log('Событие 1 произошло! (Второй обработчик)'); }, false);

// Вызываем событие
f(); // "Событие 1 произошло!" и "Событие 1 произошло! (Второй обработчик)"

// Удаляем один из обработчиков
removeEventListener('event1', y, false)

// Снова вызываем событие
f(); // "Событие 1 произошло! (Второй обработчик)"

// Удаляем последний обработчик
removeEventListener('event1', z, false);

// Вызываем событие
f(); // Ничего не выводит

// Удаляем событие
removeEvent('event1');

// Пробуем вызвать событие
try { f(); } catch(/** @type Error */ e) { console.error(e.message); } // Выведет в консоль сообщение об ошибке: "Событие 'event1' было удалено!"
Примерно так. Добавлю ещё, что функция removeEventListener возвращает значение типа boolean, означающее наличие, либо отсутствие иных обработчиков данного события в настоящий момент.

Без лишних предисловий, вот основной код функции:
/** Функция делает переданный ей объект Observer`ом, т.е. генератором событий, на которые можно подписываться и
 * отписываться. После её вызова у переданного ей объекта появляются 3 метода: <code>addEvent(name)</code> для
 * добавления событий и пара функций <code>{@link #addEventListener}</code> и
 * <code>{@link #removeEventListener}</code> для добавления и удаления обработчиков событий.
 * @param {Object} that любой объект JavaScript, который нужно сделать Observer`ом.
 * @returns {Object} переданный в качестве аргумента, обогащённый методами <code>{@link #addEvent}</code>,
 * <code>{@link #addEventListener}</code> и <code>{@link #removeEventListener}</code> объект
 * <code><em>that</em></code>. */
function setAddEvent(that) {

    var /** @type Object<Object<Function[]>>*/ events = {};

    /** Цепляет к объекту событие, на которое после этого можно будет вешать обработчики при помощи метода
     * <code>{@link #addEventListener}</code>.
     * @param {string[]} name имя события (которое потом нужно будет передавать первым параметром - type - в метод
     * <code>addEventListener</code>).
     * @returns function(string, boolean) функция, которую нужно вызывать в ответ на событие, передавая ей при
     * вызове объект события. Функция будет вызывать все слушатели по порядку, передавая им этот объект. */
    that.addEvent = function(name) {

        if (name in events)
            throw new Error('Cобытие ' + name + ' уже есть!');

        /** @type Function[] */ (
            /** @type Object<Function[]> */ events[name] = {}
        )[true] = [];
        /** @type Function[] */ events[name][false] = [];

        return function(event, isPropagation) {
            if (!(name in events)) throw new Error('Событие \'' + name + '\' было удалено!');
            if (typeof isPropagation === 'undefined') isPropagation = false;
            for (var /** @type Function[] */ listeners = events[name][isPropagation],
                     /** @type number     */ i = 0,
                     /** @type Function   */ listener;
                 listener = listeners[i++];)
                listener(event);
        };
    };

    /** Удаляет событие и всех его слушателей из объекта.
     * @param {string} name Имя события, назначенное ему ранее при вызове метода <code>{@link #addEvent}</code>.*/
    that.removeEvent = function(name) {
        if (!(name in events))
            throw new Error(
                'События ' + name + ' уже итак нет, либо оно не было создано при помощи метода \'addEvent\'!');

        delete events[name];
    };

    /** Добавляет к объекту слушателя события, ранее созданного при помощи вызова метода
     * <code>{@link #addEvent}</code>. Если метод <code>addEventListener</code> у объекта уже есть, данный метод
     * выступает в качестве его Proxy, сохраняя его в замыкании и вызывая его для событий, которые не были созданы
     * при помощи вызова метода <code>{@link #addEvent}</code>.
     * @param {string} eventName Имя события объекта
     * @param {function(Event)} handler Обработчик события
     * @param {boolean} [isPropagation=false]
     * @returns {function(Event)} Параметр, переданный функции вторым - handler. */
    that.addEventListener = function(realSubject) {
        return function(eventName, handler, isPropagation){
            if (typeof isPropagation === 'undefined') isPropagation = false;
            if (eventName in events)
                events[eventName][isPropagation].push(handler);
            else
            if (typeof realSubject === 'function')
                realSubject.apply(this, arguments);

            return handler;
        };
    }(that.addEventListener);

    /** Удаляет из объекта обработчик события, ранее созданного при помощи вызова метода
     * <code>{@link #addEvent}</code>. Если метод <code>removeEventListener</code> у объекта уже есть, данный метод
     * выступает в качестве его Proxy, сохраняя его в замыкании и вызывая его для событий, которые не были созданы
     * при помощи вызова метода <code>{@link #addEvent}</code>.
     * @param {string} eventName Имя события объекта
     * @param {function(Event)} handler Обработчик события
     * @param {boolean} [isPropagation=false]
     * @returns {boolean} Остались ли ещё обработчики для данного события? Возврат значения <code>false</code>
     * указывает на бессмысленность дальнейшего вызова функции, возвращённой методом <code>{@link #addEvent}</code>
     * при создании данного события - пока при помощи метода <code>{@link #addEventListener}</code> не будет
     * прикреплён хотя бы один новый обработчик. */
    that.removeEventListener = function(realSubject) {
        return function(eventName, handler, isPropagation){
            if (typeof isPropagation === 'undefined') isPropagation = false;
            var /** @type Function[] */ listeners,
                /** @type number     */ index;
            if (eventName in events &&
                (index = (listeners = events[eventName][isPropagation]).indexOf(handler)) !== -1)
                listeners.splice(index, 1);
            else
            if (typeof realSubject === 'function')
                realSubject.apply(this, arguments);

            return listeners.length > 0 || events[eventName][!isPropagation].length > 0;
        };
    }(that.removeEventListener);

    return that;
}

//Test
var f = setAddEvent(this).addEvent('event1'),
    y = addEventListener('event1', function(){ console.log('Событие 1 произошло!'); }),
    z = addEventListener('event1', function(){ console.log('Событие 1 произошло! (Второй обработчик)'); }, false);
f();
addEventListener('load', function(){ console.log('loaded'); }, false);
console.log( 'Остались ли ещё обработчики? - ' + (
    removeEventListener('event1', y, false)
        ? 'Да': 'Нет'
    ));
f();
console.log( 'Остались ли ещё обработчики? - ' + (
    removeEventListener('event1', z, false)
        ? 'Да': 'Нет'
    ));
f();
removeEvent('event1');
try { f(); } catch(/** @type Error */ e) { console.error(e.message); }