Обработчики событий 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 уже так хорошо ответил на вопрос.
Ответ 4
.data( "events" ) был удален в версиях 1.9 и 2.0beta, поэтому вы не можете больше полагаться на эти решения.
http://jquery.com/upgrade-guide/1.9/#data-quot-events-quot-
Ответ 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());
});
});
};
Можно экстраполировать на это методы, которые помещают данный обработчик до или после некоторого другого данного обработчика.