单个元素上有多个JS事件处理程序

35

我正在与一个既有的网络应用程序一起工作,在该应用程序中有各种不同表单上的提交按钮,有些使用常规的HTTP POST,有些定义了onClick函数,还有一些是使用元素上的类绑定了js事件处理程序。

我想做的是,通过向这些按钮添加一个类,将另一个事件处理程序绑定到这些按钮上,但我想确定的是,新的事件处理程序是否保证被执行,或者其中一个表单提交操作是否会在它之前发生,意味着我的新函数没有执行。

举个例子,我想给这些按钮添加一个类,将它们全部绑定到一个公共js函数上,该函数仅将使用情况记录到某个API中。是否存在风险,即由于表单提交导航离开页面而未调用日志函数?

我没有做过大量的js开发,我可以测试100次以上,并幸运地得到它的触发。


以下是我针对其中一个示例进行测试的一些代码 - 再次强调,我不是在询问如何绑定多个事件,问题是关于规范的理解以及是否保证执行所有处理程序。

$(document).ready(function(){
    $('.testingBtn').click(function() {
        window.location.replace("http://stackoverflow.com");
    });
    $( ".testingBtn" ).click(function(){
        alert('submitting!');
    });
});


<input class="testingBtn" type="submit" id="submitform" value="Complete Signup" />

如上所述,我可以绑定多个事件,在本例中仅指向另一个URL,但这可以是form.submit()等等。在我的测试中,警报始终首先触发,但我只是在赛跑条件方面运气好吗?


1
你是否正在使用jQuery或其他DOM和事件库?因为这通常是这些库极其优雅覆盖的一个案例。 - Alex Wayne
是的查询,不过如果有更适合的库,我很乐意听听。 - rhinds
"query"? 你是指 "jQuery" 吗? - Alex Wayne
是的,抱歉。我在使用手机。 - rhinds
@rhinds:抱歉,我有点受不了看到那么多懒惰的问题。我删除了评论,并添加了一个VanillaJS答案,附带了一篇必读的关于事件、事件顺序等方面的文章链接... - Elias Van Ootegem
@EliasVanOotegem 没问题,我理解你的痛苦 - 感谢你的答案,非常有帮助。 - rhinds
3个回答

68

在JS中,你并不能真正控制事件处理程序的调用顺序,但是通过仔细委派和恰当的监听器,这是可能的。

委派是事件模型中最强大的功能之一。正如你可能已经知道或不知道的那样,在JS中,事件被传递到DOM的顶部,然后向下传播到应该应用事件的元素。因此,附加到全局对象的事件侦听器将在附加到元素本身的侦听器之前调用其处理程序。

window.addEventListener('click',function(e)
{
    e = e || window.event;
    var target = e.target || e.srcElement;
    console.log('window noticed you clicked something');
    console.log(target);//<-- this is the element that was clicked
}, false);//<-- we'll get to the false in a minute

需要注意的是,实际上我们在处理程序中可以访问事件对象。在这种情况下,我们将事件对象保持不变,因此它将继续向目标传播,在其下行的过程中,可能会遇到类似于这样的内容:

document.getElementById('container').addEventListener('click', function(e)
{
    e = e || window.event;
    var target = e.target || e.srcElement;
    if (target.tagName.toLowerCase() !== 'a' || target.className.match(/\bclickable\b/))
    {
        return e;//<return the event, unharmed
    }
    e.returnValue = false;
    if (e.preventDefault)
    {
        e.preventDefault();
    }
}, false);

现在,当窗口级别的监听器调用其帮助程序后,将调用此处理程序。如果单击的元素没有 clickable 类或该元素是链接,则此时事件 已被更改。事件被取消了,但仍然存在。事件仍然可以自由地向下传播到 DOM,因此我们可能会遇到类似以下内容:

document.getElmentById('form3').addEventListener('click',function(e)
{
     e = e || window.event;
     if (e.returnValue === false || e.isDefaultPrevented)
     {//this event has been changed already
         //do stuff, like validation or something, then you could:
         e.cancelBubble = true;
         if (e.stopPropagation)
         {
             e.stopPropagation();
         }
     }
}, false);

通过调用stopPropagation,事件被取消了。除非事件已经改变,否则它不能向下传播到目标dom。如果没有改变,事件对象会继续向下传递,好像什么也没发生。

一旦到达目标节点,事件进入第二阶段:冒泡阶段。它不是向DOM深处传播,而是爬回到顶层(一直到全局对象,从那里分派出...之后又来了)。

在冒泡阶段,所有相同的规则都应用于传播阶段,只是相反的方向。事件对象将首先遇到最靠近目标元素的元素,最后是全局对象。

这里有很多实用和清晰的图示。我无法比quirksmode更好地解释,建议您阅读他们在那里所说的内容。

底线:当处理2个事件侦听器时,请将它们都附加在不同的级别上,以按您喜欢的方式对它们进行排序。

如果您想保证两者都被调用,请仅在将被最后调用的处理程序中停止事件传播。

当您有两个侦听器,附加到同一元素/对象以进行相同的事件处理时,我从未遇到过首次附加的侦听器不会首先被调用的情况。

就这些了,我要去睡觉了,希望我说得有道理。


即使我不是提问者,你的回答也非常有用。谢谢。 - abderrahim_05
关于“<--我们一会儿会谈到false”,这是来自https://www.quirksmode.org/js/events_order.html的解释:如果最后一个参数为true,则事件处理程序设置为捕获阶段,如果为false,则事件处理程序设置为冒泡阶段。 - Gabriel Reguly

13

jQuery 让这变得简单。

$(document).on('click', '.someclass', function() {
  doStuff();
});

$(document).on('click', '.someclass', function() {
  doMoreStuff();
});

然后,两个处理程序都将在单击时触发。jQuery为您保留了手动队列。并处理与您选择的选择器匹配的文档点击,以便无论何时创建按钮都可以触发它们。


谢谢Alex - 那么JS是否保证doStuff()和doMoreStuff()都会执行完毕?即使doStuff重定向到另一个URL,doMoreStuff()是否仍然会在它之前完成?(我已经用示例更新了我的问题) - rhinds
在大多数情况下是肯定的。但当涉及到离开页面时,我不确定。你可以测试一下并找出答案! - Alex Wayne
我想补充一点,如果你的代码需要在你开始离开页面后才能运行,那么我建议你重新考虑你的方法...那似乎是一个不好的主意。 - Alex Wayne
是的,我在考虑我可能需要改变策略 - 希望这样做能够奏效的唯一原因是因为我想将此日志记录功能添加到我的现有代码中的按钮上,并且仅向它们添加一个类会比在提交表单之前更新所有按钮以调用日志记录 API 更好。 - rhinds
谢谢,这个方法可以添加事件而不覆盖现有的事件! - Janpan

4
我有一个类似的问题。然而,我无法影响/委托预先由Wicket框架添加的“click”事件的顺序。 但是,在框架处理任何“click”或“change”事件之前,我仍然需要执行一个新的自定义事件。
幸运的是,有几个事件实际上是按顺序执行的。“mousedown”和“mouseup”恰好在“click”之前发生。 http://en.wikipedia.org/wiki/DOM_events
$(document).on('mousedown', function (event) {
  event = event || window.event
  var target = event.target || event.srcElement;
  console.log(target + ' before default event'); // Hold mouse button down to see this message in console before any action is executed
});

或者

$(document).on('mouseup', function (event) {
  event = event || window.event
  var target = event.target || event.srcElement;
  alert(target + ' before default event'); // You may not notice this event fires when the page changes unless this is an alert
});

这样可以允许在实际事件执行之前(例如通过ajax)记录日志,比如通过(ajax)链接进行页面更改。

当然,您可能需要更复杂的手段来检测需要进行额外事件处理的内容,但您可以使用“target”信息来��现。 => 此脚本监视页面上的所有内容,因为这是我需要的方法。


哦,@ville,你真是个传奇。我花了好几天的时间才找到这个。Materializecss 的 sideNav 函数似乎会破坏动态内容上的点击事件。对于最初加载的内容来说,它可以正常工作,但是我使用 $(document).on('click', '.second-class-to-hook-into-for-own-function'... 时,它们从未触发。使用 mouseup 而不是 click,让我的其他函数在框架触发之前执行,并实际提高了用户的感知速度。谢谢! - Solvision

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接