Обработчики событий jQuery всегда выполняются так, чтобы они были связаны - каким-то образом?

Это может быть связано с тем, что обработчики событий jQuery всегда выполняются в том порядке, в котором они были связаны. Например:

$('span').click(doStuff1);
$('span').click(doStuff2);

нажатие на пробел приведет к срабатыванию doStuff1(), затем doStuff2().

В то время, когда я связываю doStuff2(), мне бы хотелось, чтобы опция привязывала его до doStuff1(), но, похоже, нет простого способа сделать это.

Я думаю, большинство людей скажут, просто напишите код следующим образом:

$('span').click(function (){
    doStuff2();
    doStuff1();
});

Но это всего лишь простой пример - на практике это не всегда удобно.

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

Итак, каков наилучший способ достичь этого в jQuery?

Ответ 1

Обновленный ответ

jQuery изменил местоположение, где хранятся события в 1.8. Теперь вы знаете, почему такая плохая идея возиться с внутренними API:)

Новый внутренний API для доступа к событиям для объекта DOM доступен через глобальный объект jQuery и не привязан к каждому экземпляру, и он принимает элемент DOM в качестве первого параметра, а ключ ( "события" для нас) в качестве второго параметра.

jQuery._data(<DOM element>, "events");

Итак, здесь изменен код для jQuery 1.8.

// [name] is the name of the event "click", "mouseover", .. 
// same as you'd pass it to bind()
// [fn] is the handler function
$.fn.bindFirst = function(name, fn) {
    // bind as you normally would
    // don't want to miss out on any jQuery magic
    this.on(name, fn);

    // Thanks to a comment by @Martin, adding support for
    // namespaced events too.
    this.each(function() {
        var handlers = $._data(this, 'events')[name.split('.')[0]];
        // take out the handler we just inserted from the end
        var handler = handlers.pop();
        // move it at the beginning
        handlers.splice(0, 0, handler);
    });
};

И здесь игровая площадка.


Оригинальный ответ

Как обнаружил @Sean, jQuery предоставляет все обработчики событий через интерфейс data. В частности element.data('events'). Используя это, вы всегда можете написать простой плагин, в котором вы можете вставить любой обработчик событий в определенную позицию.

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

// [name] is the name of the event "click", "mouseover", .. 
// same as you'd pass it to bind()
// [fn] is the handler function
$.fn.bindFirst = function(name, fn) {
    // bind as you normally would
    // don't want to miss out on any jQuery magic
    this.bind(name, fn);

    // Thanks to a comment by @Martin, adding support for
    // namespaced events too.
    var handlers = this.data('events')[name.split('.')[0]];
    // take out the handler we just inserted from the end
    var handler = handlers.pop();
    // move it at the beginning
    handlers.splice(0, 0, handler);
};

Так, например, для этой разметки она будет работать как (пример здесь):

<div id="me">..</div>

$("#me").click(function() { alert("1"); });
$("#me").click(function() { alert("2"); });    
$("#me").bindFirst('click', function() { alert("3"); });

$("#me").click(); // alerts - 3, then 1, then 2

Однако, так как .data('events') не является частью своего общедоступного API, насколько я знаю, обновление jQuery может сломать ваш код, если базовое представление вложенных событий изменится с массива на что-то иначе, например.

Отказ от ответственности: поскольку все возможно:), вот ваше решение, но я все равно ошибаюсь на стороне рефакторинга существующего кода, так как просто попытка запомнить порядок, в котором эти элементы были присоединены, вскоре может выйти из-под контроля, поскольку вы продолжаете добавлять все больше и больше этих упорядоченных событий.

Ответ 2

Вы можете создать собственное пространство имен событий.

$('span').bind('click.doStuff1',function(){doStuff1();});
$('span').bind('click.doStuff2',function(){doStuff2();});

Затем, когда вам нужно вызвать их, вы можете выбрать порядок.

$('span').trigger('click.doStuff1').trigger('click.doStuff2');

или

$('span').trigger('click.doStuff2').trigger('click.doStuff1');

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

$('span').trigger('click'); 

Ответ 3

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

Глядя на исходный код для jQuery 1.4.2, я видел этот блок между строками 2361 и 2392:

jQuery.each(["bind", "one"], function( i, name ) {
    jQuery.fn[ name ] = function( type, data, fn ) {
        // Handle object literals
        if ( typeof type === "object" ) {
            for ( var key in type ) {
                this[ name ](key, data, type[key], fn);
            }
            return this;
        }

        if ( jQuery.isFunction( data ) ) {
            fn = data;
            data = undefined;
        }

        var handler = name === "one" ? jQuery.proxy( fn, function( event ) {
            jQuery( this ).unbind( event, handler );
            return fn.apply( this, arguments );
        }) : fn;

        if ( type === "unload" && name !== "one" ) {
            this.one( type, data, fn );

        } else {
            for ( var i = 0, l = this.length; i < l; i++ ) {
                jQuery.event.add( this[i], type, handler, data );
            }
        }

        return this;
    };
});

Здесь интересный материал много, но интересующая нас часть находится между строками 2384 и 2388:

else {
    for ( var i = 0, l = this.length; i < l; i++ ) {
        jQuery.event.add( this[i], type, handler, data );
    }
}

Каждый раз, когда мы вызываем bind() или one(), мы фактически вызываем вызов jQuery.event.add()... поэтому давайте взглянем на это (строки с 1557 по 1672, если вам интересно)

add: function( elem, types, handler, data ) {
// ... snip ...
        var handleObjIn, handleObj;

        if ( handler.handler ) {
            handleObjIn = handler;
            handler = handleObjIn.handler;
        }

// ... snip ...

        // Init the element event structure
        var elemData = jQuery.data( elem );

// ... snip ...

        var events = elemData.events = elemData.events || {},
            eventHandle = elemData.handle, eventHandle;

        if ( !eventHandle ) {
            elemData.handle = eventHandle = function() {
                // Handle the second event of a trigger and when
                // an event is called after a page has unloaded
                return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
                    jQuery.event.handle.apply( eventHandle.elem, arguments ) :
                    undefined;
            };
        }

// ... snip ...

        // Handle multiple events separated by a space
        // jQuery(...).bind("mouseover mouseout", fn);
        types = types.split(" ");

        var type, i = 0, namespaces;

        while ( (type = types[ i++ ]) ) {
            handleObj = handleObjIn ?
                jQuery.extend({}, handleObjIn) :
                { handler: handler, data: data };

            // Namespaced event handlers
                    ^
                    |
      // There is is! Even marked with a nice handy comment so you couldn't miss it 
      // (Unless of course you are not looking for it ... as I wasn't)

            if ( type.indexOf(".") > -1 ) {
                namespaces = type.split(".");
                type = namespaces.shift();
                handleObj.namespace = namespaces.slice(0).sort().join(".");

            } else {
                namespaces = [];
                handleObj.namespace = "";
            }

            handleObj.type = type;
            handleObj.guid = handler.guid;

            // Get the current list of functions bound to this event
            var handlers = events[ type ],
                special = jQuery.event.special[ type ] || {};

            // Init the event handler queue
            if ( !handlers ) {
                handlers = events[ type ] = [];

                   // ... snip ...

            }

                  // ... snip ...

            // Add the function to the element handler list
            handlers.push( handleObj );

            // Keep track of which events have been used, for global triggering
            jQuery.event.global[ type ] = true;
        }

     // ... snip ...
    }

В этот момент я понял, что понимание этого займет более 30 минут... поэтому я искал Stackoverflow для

jquery get a list of all event handlers bound to an element

и нашел этот ответ для итерации по связанным событиям:

//log them to the console (firebug, ie8)
console.dir( $('#someElementId').data('events') );

//or iterate them
jQuery.each($('#someElementId').data('events'), function(i, event){

    jQuery.each(event, function(i, handler){

        console.log( handler.toString() );

    });

});

Тестирование того, что в Firefox я вижу, что объект events в атрибуте data для каждого элемента имеет атрибут [some_event_name] (click в нашем случае), к которому прикреплен массив объектов handler каждый из которых имеет ориентир, пространство имен, тип и обработчик. "Итак, я думаю," мы должны теоретически иметь возможность добавлять объекты, построенные таким же образом, к [element].data.events.[some_event_name].push([our_handler_object);... "

И затем я закончу писать свои выводы... и найду лучший ответ , опубликованный RusselUresti..., который вводит меня в нечто новое, что я не знал о jQuery ( хотя я смотрел прямо в лицо.)

Что является доказательством того, что Stackoverflow - лучший сайт вопросов и ответов в Интернете, по крайней мере, по моему скромному мнению.

Итак, я отправляю это ради потомства... и отмечаю, что это сообщество wiki, так как RussellUresti уже так хорошо ответил на вопрос.

Ответ 5

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

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

Ответ 6

Для jQuery 1.9+ как Dunstkreis указан .data('events') удален. Но вы можете использовать другой хак (не рекомендуется использовать недокументированные возможности) $._ data ($ (this).get(0), 'events'), а решение, предоставляемое anurag, будет выглядеть так:

$.fn.bindFirst = function(name, fn) {
    this.bind(name, fn);
    var handlers = $._data($(this).get(0), 'events')[name.split('.')[0]];
    var handler = handlers.pop();
    handlers.splice(0, 0, handler);
};

Ответ 7

Выбранный ответ, созданный Anurag, является лишь частично правильным. Из-за некоторых внутренних функций обработки событий jQuery предлагаемая функция bindFirst не будет работать, если у вас есть сочетание обработчиков с фильтрами и без них (например: $(document).on("click", обработчик) против $(document).on("click", "button", обработчик)).

Проблема заключается в том, что jQuery поместит (и ожидает), что первые элементы массива обработчика будут такими обработанными фильтрами, поэтому размещение нашего события без фильтра в начале разрывает эту логику, и все начинает разваливаться. Обновленная функция bindFirst должна быть следующей:

$.fn.bindFirst = function (name, fn) {
    // bind as you normally would
    // don't want to miss out on any jQuery magic
    this.on(name, fn);

    // Thanks to a comment by @Martin, adding support for
    // namespaced events too.
    this.each(function () {
        var handlers = $._data(this, 'events')[name.split('.')[0]];
        // take out the handler we just inserted from the end
        var handler = handlers.pop();
        // get the index of the first handler without a selector
        var firstNonDelegate = handlers.first(function(h) { return !h.selector; });
        var index = firstNonDelegate ? handlers.indexOf(firstNonDelegate)
                                     : handlers.length; // Either all handlers are selectors or we have no handlers
        // move it at the beginning
        handlers.splice(index, 0, handler);
    });
};

Ответ 8

Я предполагаю, что вы говорите об аспекте пузыря. Было бы полезно увидеть ваш HTML для указанных span элементов. Я не понимаю, почему вы хотите изменить основное поведение, как это, я не считаю это совершенно раздражающим. Предлагаю перейти ко второму блоку кода:

$('span').click(function (){
  doStuff2();
  doStuff1();
});

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

Ответ 9

Здесь решение для jQuery 1.4.x (к сожалению, принятый ответ не работал для jquery 1.4.1)

$.fn.bindFirst = function(name, fn) {
    // bind as you normally would
    // don't want to miss out on any jQuery magic
    this.bind(name, fn);

    // Thanks to a comment by @Martin, adding support for
    // namespaced events too.
    var handlers = this.data('events')[name.split('.')[0]];
    // take out the handler we just inserted from the end
    var copy = {1: null};

    var last = 0, lastValue = null;
    $.each(handlers, function(name, value) {
        //console.log(name + ": " + value);
        var isNumber = !isNaN(name);
        if(isNumber) {last = name; lastValue = value;};

        var key = isNumber ? (parseInt(name) + 1) : name;
        copy[key] = value;
    });
    copy[1] = lastValue;
    this.data('events')[name.split('.')[0]] = copy;
};

Ответ 10

Совет Криса Чилверса должен быть первым курсом действий, но иногда мы имеем дело с сторонними библиотеками, которые делают это сложным и требуют от нас совершать непослушные вещи... что это такое. ИМО это преступление презумпции, похожее на использование! Важно в CSS.

Сказав, что, основываясь на ответе Анурага, вот несколько дополнений. Эти методы допускают множественные события (например, "keydown keyup paste" ), произвольное позиционирование обработчика и переупорядочение после факта.

$.fn.bindFirst = function (name, fn) {
    this.bindNth(name, fn, 0);
}

$.fn.bindNth(name, fn, index) {
    // Bind event normally.
    this.bind(name, fn);
    // Move to nth position.
    this.changeEventOrder(name, index);
};

$.fn.changeEventOrder = function (names, newIndex) {
    var that = this;
    // Allow for multiple events.
    $.each(names.split(' '), function (idx, name) {
        that.each(function () {
            var handlers = $._data(this, 'events')[name.split('.')[0]];
            // Validate requested position.
            newIndex = Math.min(newIndex, handlers.length - 1);
            handlers.splice(newIndex, 0, handlers.pop());
        });
    });
};

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