JQuery 事件处理程序总是按照绑定的顺序执行——有什么办法可以解决这个问题吗?

JQuery 事件处理程序总是按照绑定的顺序执行,这可能会让人感到厌烦。例如:

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

单击 span 将导致触发 doStuff1(),然后是 doStuff2()

在绑定 doStuff2()时,我希望选择绑定它 在此之前 doStuff1() ,但似乎没有任何简单的方法可以做到这一点。

我想大多数人会说,像这样编写代码:

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

但这只是一个简单的例子——在实践中,这样做并不总是方便的。

有些情况下需要绑定事件,而绑定到的对象已经具有事件。在这种情况下,您可能只是希望在任何其他现有事件之前触发新事件。

那么,在 jQuery 中实现这一点的最佳方法是什么呢?

112339 次浏览

我假设你说的是事件冒泡的方面。如果能够查看所述 span元素的 HTML,也会很有帮助。我不明白你为什么要这样改变核心行为,我一点也不觉得讨厌。我建议使用第二块代码:

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

最重要的是,我认为如果像您所说明的那样管理同一块中给定元素的所有事件,您会发现它更有组织性。你能解释一下为什么你觉得这很烦人吗?

您可以执行事件的自定义命名空间。

$('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');

此外,只要触发点击应该按照他们被绑定的顺序触发... 所以你仍然可以这样做

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

标准原则是单独的事件处理程序不应该依赖于它们被调用的顺序。如果它们确实取决于顺序,它们就不应该是分开的。

否则,您将一个事件处理程序注册为“第一个”,然后其他人将其事件处理程序注册为“第一个”,您将重新陷入与之前相同的混乱局面。

一个非常好的问题... ... 我很感兴趣,所以我做了一些调查; 对于那些感兴趣的人,这是我去过的地方,以及我想到的。

在查看 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'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 ...
}

在这一点上,我意识到理解这将需要超过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 中测试,我看到每个元素的 data属性中的 events对象都有一个 [some_event_name]属性(在我们的例子中是 click) ,它附加了一个 handler对象数组,每个对象都有一个 guid、一个名称空间、一个类型和一个处理程序。“所以”,我认为,“理论上我们应该能够添加以同样方式构建的对象到 [element].data.events.[some_event_name].push([our_handler_object);...”

然后我继续写我的发现... ... 并且找到一个由 RusselUresti 发布的 很多更好的答案... ... 它向我介绍了一些我不知道的关于 jQuery 的新东西(即使我当时正盯着它的脸)

这证明 Stackoverflow 是互联网上最好的问答网站,至少在我看来是这样。

所以为了子孙后代的利益,我发布了这篇文章,并标记为社区维基,因为拉塞尔 · 乌雷斯蒂已经很好地回答了这个问题。

更新答案

JQuery 更改了1.8中事件存储的位置。现在您知道为什么搞乱内部 API 是一个非常糟糕的主意了:)

新的 内部 API 可以通过全局 jQuery 对象访问 DOM 对象的事件,而不是绑定到每个实例,它将 DOM 元素作为第一个参数,键(对我们来说是“ events”)作为第二个参数。

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 的更新可能会破坏你的代码。

免责声明: 因为任何事情都是可能的:) ,这是你的解决方案,但是我仍然会错误地重构你现有的代码,因为只是试图记住这些项目的附加顺序很快就会失去控制,因为你不断添加越来越多的这些有序事件。

这是一个 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;
};

. data (“ events”)已经在1.9和2.0 beta 版本中删除,因此您不能再依赖这些解决方案。

Http://jquery.com/upgrade-guide/1.9/#data-quot-events-quot-

对于 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);
};

Chris Chilvers 的建议应该是行动的第一步,但有时我们面对的是第三方库,这让我们面临挑战,并要求我们做一些淘气的事情... ... 这就是。我的天,这是一种类似于使用推定的犯罪!在 CSS 中很重要。

话虽如此,基于阿努拉格的回答,这里还有一些补充。这些方法允许多个事件(例如“ keydown keyup 粘贴”) ,处理程序的任意定位和事后重新排序。

$.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());
});
});
};

我们可以用一些方法来推断这一点,这些方法将把给定的处理程序放在其他给定的处理程序之前或之后。

阿努拉格所选择的答案只是部分正确。由于 jQuery 事件处理的一些内部特性,如果您混合使用带过滤器和不带过滤器的处理程序(例如: $(document)) ,那么提议的 bindFirst 函数将无法工作。在(“单击”,处理程序) vs $(文档)。(“点击”、“按钮”、处理程序)。

问题在于 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.find(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);
});
};