jQuery 事件处理程序总是按照它们被绑定的顺序执行——有什么办法可以解决这个问题?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/2360655/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
jQuery event handlers always execute in order they were bound - any way around this?
提问by asgeo1
It can be anoying that jQuery event handlers always execute in the order they were bound. For example:
jQuery 事件处理程序总是按照它们被绑定的顺序执行,这可能很烦人。例如:
$('span').click(doStuff1);
$('span').click(doStuff2);
clicking on the span will cause doStuff1()
to fire, followed by doStuff2()
.
单击跨度将导致doStuff1()
触发,然后是doStuff2()
。
At the time I bind doStuff2(), I would like the option to bind it beforedoStuff1(), but there doesn't appear to be any easy way to do this.
在我绑定 doStuff2() 时,我想要在doStuff1()之前绑定它的选项,但似乎没有任何简单的方法可以做到这一点。
I suppose most people would say, just write the code like this:
我想大多数人会说,只需像这样编写代码:
$('span').click(function (){
doStuff2();
doStuff1();
});
But this is just a simple example - in practise it is not always convienient to do that.
但这只是一个简单的例子——在实践中,这样做并不总是很方便。
There are situations when you want to bind an event, and the object you are binding to already has events. And in this case you may simply want the new event to fire before any other existing events.
在某些情况下,您想要绑定一个事件,而您要绑定的对象已经有事件。在这种情况下,您可能只希望新事件在任何其他现有事件之前触发。
So what is the best way to achieve this in jQuery?
那么在 jQuery 中实现这一目标的最佳方法是什么?
采纳答案by Anurag
Updated Answer
更新答案
jQuery changed the location of where events are stored in 1.8. Now you know why it is such a bad idea to mess around with internal APIs :)
jQuery 在 1.8 中更改了存储事件的位置。现在你知道为什么搞乱内部 API 是个坏主意了 :)
The new internalAPI to access to events for a DOM object is available through the global jQuery object, and not tied to each instance, and it takes a DOM element as the first parameter, and a key ("events" for us) as the second parameter.
访问 DOM 对象事件的新内部API 可通过全局 jQuery 对象获得,而不是绑定到每个实例,它以 DOM 元素作为第一个参数,并以一个键(我们的“事件”)作为参数第二个参数。
jQuery._data(<DOM element>, "events");
So here's the modified code for jQuery 1.8.
所以这是 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);
});
};
And here's a playground.
这里是游乐场。
Original Answer
原答案
As @Sean has discovered, jQuery exposes all event handlers through an element's data
interface. Specifically element.data('events')
. Using this you could always write a simple plugin whereby you could insert any event handler at a specific position.
正如@Sean 发现的那样,jQuery 通过元素的data
接口公开所有事件处理程序。具体来说element.data('events')
。使用它你总是可以编写一个简单的插件,你可以在特定位置插入任何事件处理程序。
Here's a simple plugin that does just that to insert a handler at the beginning of the list. You can easily extend this to insert an item at any given position. It's just array manipulation. But since I haven't seen jQuery's source and don't want to miss out on any jQuery magic from happening, I normally add the handler using bind
first, and then reshuffle the array.
这是一个简单的插件,它只是在列表的开头插入一个处理程序。您可以轻松扩展它以在任何给定位置插入项目。这只是数组操作。但是由于我还没有看到 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);
};
So for example, for this markup it would work as (example here):
因此,例如,对于这个标记,它可以作为(这里的例子):
<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
However, since .data('events')
is not part of their public API as far as I know, an update to jQuery could break your code if the underlying representation of attached events changes from an array to something else, for example.
但是,由于.data('events')
据我所知不是他们公共 API 的一部分,例如,如果附加事件的底层表示从数组更改为其他内容,则对 jQuery 的更新可能会破坏您的代码。
Disclaimer: Since anything is possible :), here's your solution, but I would still err on the side of refactoring your existing code, as just trying to remember the order in which these items were attached can soon get out of hand as you keep adding more and more of these ordered events.
免责声明:因为一切皆有可能:),这是您的解决方案,但我仍然会在重构现有代码方面犯错,因为只是试图记住这些项目的附加顺序可能很快就会失控,因为您不断添加这些有序事件越来越多。
回答by RussellUresti
You can do a custom namespace of events.
您可以自定义事件命名空间。
$('span').bind('click.doStuff1',function(){doStuff1();});
$('span').bind('click.doStuff2',function(){doStuff2();});
Then, when you need to trigger them you can choose the order.
然后,当您需要触发它们时,您可以选择顺序。
$('span').trigger('click.doStuff1').trigger('click.doStuff2');
or
或者
$('span').trigger('click.doStuff2').trigger('click.doStuff1');
Also, just triggering click SHOULD trigger both in the order they were bound... so you can still do
另外,只触发点击应该按照它们被绑定的顺序触发......所以你仍然可以这样做
$('span').trigger('click');
回答by Sean Vieira
A very good question ... I was intrigued so I did a little digging; for those who are interested, here's where I went, and what I came up with.
一个很好的问题......我很感兴趣,所以我做了一些挖掘;对于那些感兴趣的人,这是我去过的地方,以及我想出的东西。
Looking at the source code for jQuery 1.4.2 I saw this block between lines 2361 and 2392:
查看 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;
};
});
There is a lotof interesting stuff going on here, but the part we are interested in is between lines 2384 and 2388:
这里有很多有趣的东西,但我们感兴趣的部分是在第 2384 行和第 2388 行之间:
else {
for ( var i = 0, l = this.length; i < l; i++ ) {
jQuery.event.add( this[i], type, handler, data );
}
}
Every time we call bind()
or one()
we are actually making a call to jQuery.event.add()
... so let's take a look at that (lines 1557 to 1672, if you are interested)
每次我们打电话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's 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's handler list
handlers.push( handleObj );
// Keep track of which events have been used, for global triggering
jQuery.event.global[ type ] = true;
}
// ... snip ...
}
At this point I realized that understanding this was going to take more than 30 minutes ... so I searched Stackoverflow for
在这一点上,我意识到理解这一点需要 30 多分钟……所以我在 Stackoverflow 中搜索
jquery get a list of all event handlers bound to an element
and found this answerfor iterating over bound events:
并找到了迭代绑定事件的答案:
//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() );
});
});
Testing that in Firefox I see that the events
object in the data
attribute of every element has a [some_event_name]
attribute (click
in our case) to which is attatched an array of handler
objects, each of which has a guid, a namespace, a type, and a handler. "So", I think, "we should theoretically be able to add objects built in the same manner to the [element].data.events.[some_event_name].push([our_handler_object);
... "
在 Firefox 中测试,我看到每个元素events
的data
属性中的对象都有一个[some_event_name]
属性(click
在我们的例子中),该属性附加了一个handler
对象数组,每个对象都有一个 guid、一个命名空间、一个类型和一个处理程序。“所以”,我认为,“理论上,我们应该能够以相同方式构建的对象添加到[element].data.events.[some_event_name].push([our_handler_object);
......”
And then I go to finish writing up my findings ... and find a muchbetter answer posted by RusselUresti ... which introduces me to something new that I didn't know about jQuery (even though I was staring it right in the face.)
然后我去写完了我的发现......并找到很多更好的答案张贴RusselUresti ......,介绍我到新的东西,我不知道的jQuery(即使我盯着它的脸右侧.)
Which is proof that Stackoverflow is the best question-and-answer site on the internet, at least in my humble opinion.
这证明 Stackoverflow 是互联网上最好的问答网站,至少在我看来是这样。
So I'm posting this for posterity's sake ... and marking it a community wiki, since RussellUresti already answered the question so well.
所以我为了后人的缘故发布这个......并将其标记为社区维基,因为RussellUresti已经很好地回答了这个问题。
回答by Chris Chilvers
The standard principle is separate event handlers shouldn't depend upon the order they are called. If they do depend upon the order they should not be separate.
标准原则是单独的事件处理程序不应依赖于它们被调用的顺序。如果它们确实取决于顺序,则它们不应分开。
Otherwise, you register one event handler as being 'first' and someone else then registers their event handler as 'first' and you're back in the same mess as before.
否则,您将一个事件处理程序注册为“第一个”,然后其他人将他们的事件处理程序注册为“第一个”,您将回到与以前一样的混乱状态。
回答by Dunstkreis
.data("events") has been removed in versions 1.9 and 2.0beta, so you cant any longer rely on those solutions.
.data("events") 已在版本 1.9 和 2.0beta 中删除,因此您不能再依赖这些解决方案。
回答by heroin
For jQuery 1.9+ as Dunstkreismentioned .data('events') was removed. But you can use another hack (it is not recommended to use undocumented possibilities) $._data($(this).get(0), 'events') instead and solution provided by anuragwill look like:
对于 jQuery 1.9+,正如Dunstkreis提到的那样,.data('events') 被删除了。但是您可以使用另一个 hack(不建议使用未记录的可能性) $._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);
};
回答by dshapiro
The selected answer authored by Anurag is only partially correct. Due to some internals of jQuery's event handling, the proposed bindFirst function will not work if you have a mix of handlers with and without filters (ie: $(document).on("click", handler) vs $(document).on("click", "button", handler)).
Anurag 撰写的所选答案仅部分正确。由于 jQuery 事件处理的某些内部结构,如果您混合使用带过滤器和不带过滤器的处理程序(即:$(document).on("click", handler) vs $(document).on,建议的 bindFirst 函数将不起作用(“点击”,“按钮”,处理程序))。
The issue is that jQuery will place (and expect) that the first elements in the handler array will be these filtered handlers, so placing our event without a filter at the beginning breaks this logic and things start to fall apart. The updated bindFirst function should be as follows:
问题是 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);
});
};
回答by moomoo
Chris Chilvers' advice should be the first course of action but sometimes we're dealing with third party libraries that makes this challenging and requires us to do naughty things... which this is. IMO it's a crime of presumption similar to using !important in CSS.
Chris Chilvers 的建议应该是第一个行动方案,但有时我们正在与第三方库打交道,这使这具有挑战性并要求我们做一些顽皮的事情......就是这样。IMO 这是一种推定犯罪,类似于在 CSS 中使用 !important 。
Having said that, building on Anurag's answer, here are a few additions. These methods allow for multiple events (e.g. "keydown keyup paste"), arbitrary positioning of the handler and reordering after the fact.
话虽如此,在 Anurag 的回答的基础上,这里有一些补充。这些方法允许多个事件(例如“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());
});
});
};
One could extrapolate on this with methods that would place a given handler before or after some other given handler.
可以使用将给定处理程序放置在其他给定处理程序之前或之后的方法对此进行推断。
回答by Miguel Ping
Here's a solution for jQuery 1.4.x(unfortunately, the accepted answer didn't work for jquery 1.4.1)
这是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;
};
回答by JP Silvashy
I'm assuming you are talking about the event bubbling aspect of it. It would be helpful to see your HTML for the said span
elements as well. I can't see why you'd want to change the core behavior like this, I don't find it at all annoying. I suggest going with your second block of code:
我假设您正在谈论它的事件冒泡方面。查看上述span
元素的HTML 也会很有帮助。我不明白你为什么要像这样改变核心行为,我一点也不觉得它烦人。我建议使用您的第二个代码块:
$('span').click(function (){
doStuff2();
doStuff1();
});
Most importantly I think you'll find it more organized if you manage all the events for a given element in the same block like you've illustrated. Can you explain why you find this annoying?
最重要的是,我认为如果像您所展示的那样在同一块中管理给定元素的所有事件,您会发现它更有条理。你能解释为什么你觉得这很烦人吗?