使用e.stopPropagation()防止事件冒泡的优缺点

8
许多人已经解释过,e.stopPropagation() 可以防止事件冒泡。然而,我很难找到为什么有人会想要或者需要在第一时间就阻止事件冒泡的原因。
在我的网站上,有许多这样的元素:
$(document.body).on('click', ".clickable", function(e){ 
   //e.stopPropagation();
  //do something, for example show a pop-up or click a link   
});

<body>
  <p>outside stuff</p>
  <button type="button" class='clickable'>
    <img src='/icon.jpg'> Do Something

  </button>
</body>

我考虑添加 e.stopPropagation(),因为我想使用 这个很棒的触控库 Hammer.js 将事件处理程序从 'click' 更改为 'touch'。这将允许在桌面上正常进行点击,并在移动设备上进行触摸事件。
问题是(请纠正我如果我错了),在触摸设备上滚动会变得缓慢。
这时候 e.stopPropgation() 有用吗?这样每次触摸屏幕时 document.body 事件冒泡不会发生?

1
你的处理程序中已经有了 return false,它已经停止了事件传播... - kapa
1
我不知道你的问题是什么。是关于为什么要使用事件冒泡吗?还是关于为什么不想使用事件冒泡?抑或是关于我是否正确地使用了stopPropagation? - Halcyon
@bažmegakapa 哦,是的,谢谢!好吧,这个例子不太好,我需要修改一下... - tim peterson
@FritsvanCampen,是的,我的问题是为什么你不想要事件冒泡?我想到的一个原因是,在触摸设备上可能会导致性能问题,因为有可能会出现太多快速的“点击”事件(手指移动得很快),每个事件都会一直冒泡到<body> - tim peterson
1
@timpeterson - 我在下面的回答中增加了很多与你在评论中提出的各种问题有关的内容。 - jfriend00
3个回答

16

在JavaScript/jQuery中,有几种处理事件的方法。其中两种是:

  1. 你可以在对象上使用直接事件处理程序。
  2. 你可以使用委托事件处理,其中你在父级上处理传播事件。

如果你在对象上使用直接事件处理程序,并且页面中没有配置委派事件处理程序,则没有理由使用 e.stopPropagation()

但是,如果你有使用传播的委托事件处理程序,有时你会想确保更高级别的委派事件处理程序不会在当前事件上触发。

在你的特定示例中:

$(document.body).on('click', "a.ajaxLink", function(e){ 
这是一个委托事件处理程序。它寻找任何传播到 document.body 对象上的 click 事件,但起源于 a.ajaxLink 对象。这里,在大部分事件已经传播的情况下,e.stopPropagation() 几乎没有优势(它也会传播到 document 上,但除非您还在 document 对象上有一个 click 处理程序,否则在此处理程序中没有理由使用 e.stopPropagation())。
当您既有顶级委派事件处理程序(如您示例中的处理程序),又有较低级别的事件处理程序(直接在对象上或使用委派事件处理,但在 document.body 对象以下的级别)时,它就有很多意义。在这种情况下,如果您只想让较低级别的事件处理程序获取事件,则会在其处理程序中调用 e.stopPropagation(),以便 document.body 处理程序永远不会看到该事件。
$("a.ajaxLink").click(function(e) {
    if (some condition) {
        // do something specific to this condition
        code here
        // stop propagation so the default behavior for click in document.body does not fire
        e.stopPropagation();
    }
})
注意:在 jQuery 的事件处理程序中使用 `return false` 会触发 `e.stopPropagation()` 和 `e.preventDefault()`。但是,如果您在委托的事件处理程序中,则 `e.preventDefault()` 不起作用,因为默认行为(如果有)已在目标对象首次看到事件时触发。默认行为发生在事件传播之前,因此 `e.preventDefault()` 仅在直接放置在目标对象上的事件处理程序中起作用。
没有明显的性能下降,因为您允许事件冒泡,因为这些是用户级事件,它们不会发生得足够快以至于无法处理,而且当所有中间对象都没有处理程序时,冒泡速度也不会特别慢。系统已经对一些可能会快速发生的事件(如 `mousemove`)进行了特殊处理来解决这个问题。如果您有一个包含数百或数千个事件处理程序的巨大项目,则使用委托事件处理可能更有效率,而在实际目标对象上使用直接事件处理程序可能更有效率。但是,除了在巨大场景中,性能差异可能并不明显。
以下是冒泡/委托更有效的示例。您有一个包含数千行的巨型表格,每行都有两个按钮(添加/删除)。使用委派事件处理程序,您可以将所有按钮都处理为两个简单的事件处理程序,将它们附加到表格对象(按钮的公共父级)。这样安装事件处理程序会比在每个按钮上直接安装几千个事件处理程序快得多。这些委托事件处理程序还将自动在表格中新创建的行/对象上工作。这是事件冒泡/委派事件处理程序的理想情况。请注意,在此场景中,没有理由停止传播/冒泡。
以下是委派事件处理程序非常低效的示例。假设您有一个规模不小的网页,其中包含数百个对象和事件处理程序。您可以使每个事件处理程序都成为附加到 `document` 对象的委托事件处理程序。但是,下面是会发生的事情。单击发生。实际对象上没有事件处理程序,因此它会冒泡上来。最终,它到达了 `document` 对象。`document` 对象有数百个事件处理程序。事件处理引擎(在这种情况下是 jQuery)必须查看每个委托事件处理程序中的选择器与原始事件目标是否匹配。这些比较中的某些可能不太快,因为它们可以是完整的 CSS 选择器。必须对数百个委托事件进行此操作。这对性能不利。这正是为什么在 jQuery 中 `live()` 被弃用的原因,因为它以这种方式工作。相反,应该尽可能靠近目标对象放置委托事件处理程序(给定情况下最接近的父级),并且当没有需要委派的事件处理程序时,应将处理程序放在实际目标对象上,因为这是运行时最有效率的。

回到您最初的问题。通常情况下,没有时间希望气泡被关闭。正如我在之前的答案中所描述的那样,有特定的情况,其中树形结构上更远处的事件处理程序想要处理该事件并停止任何委托给DOM树中更高级别的事件处理程序来处理此事件的情况。这是调用e.stopPropatation()的时候。


以下是一些相关的帖子,它们提供了有关此主题的有用信息(因为此话题已经被广泛讨论):

为什么不将JavaScript事件委派推向极致?

是否应将所有jQuery事件绑定到$(document)?

jQuery.on()对于在创建事件处理程序之后添加的元素是否有效?

jQuery on()和stopPropagation()

避免绑定大量DOM对象以进行单击事件而导致的内存或性能问题的最佳实践

jQuery.live()与.on()方法在加载动态html后添加单击事件的区别


@TimPeterson - 我在我的答案末尾添加了更多内容,以回答你关于是否想要停止传播以提高性能的问题。 - jfriend00
-@jfriend00,谢谢你的出色回答。好的,我想我明白你的意思了。我们可以讨论一下最后一部分,“相反,委托事件处理程序...”吗?所以最佳性能是使用$('.ajaxLink').click(..。理想情况下,如果我有成百上千个链接,我不会使用$(document.body).on('click', "a.ajaxLink",..,而是更接近每个链接的某些东西,像这样:$('#parentDiv').on('click', "a.ajaxLink",..?主要问题是许多这些链接是动态创建的,所以我无法进行直接的事件处理,只能使用on()作为一个万能方法。你有什么想法吗? - tim peterson
1
@timpeterson - 听起来你已经有了大致的想法。对于动态创建的对象,最简单的方法是使用委托事件处理,但是你应该将委托事件处理程序附加到最近的共同父对象上,而不是它本身是动态的(通常是某个容器对象)。如果可以避免,你不希望在同一高级对象(例如 documentdocument.body)上有很多委托事件处理程序。 - jfriend00
@timpeterson - 我添加了一堆与同一主题相关的其他帖子链接。 - jfriend00
-@jfriend00,听起来不错,谢谢你的帮助。我学到了很多。 - tim peterson

2
想象一下你有一个按钮,想要处理点击事件:
<button>
    <img src="icon" />
    <span>Text</span>
</button>

如果没有事件传播,点击图像或文本不会触发绑定到按钮的单击事件处理程序,因为事件永远不会离开<img><span>元素。


你不希望事件传播的情况是嵌套元素具有自己的事件处理程序。以下是一个例子:

<button>
    <img src="icon" />
    <span>Text</span>
    <span class="arrow">&darr;</span>
</button>

如果您不在.arrow的事件处理程序中停止事件传播,它也会触发按钮的事件处理程序。

我明白,但这不是我的代码(或者我认为大多数人的代码)看起来的样子。在所有情况下,我都使用类名,例如.ajaxLink,这些类名被命名为触发特定处理程序。 - tim peterson
@timpeterson:如果您的元素有子元素,该怎么办?我真的怀疑您不使用事件传播。 - Blender
我添加了一些HTML以帮助我们讨论,因为我对你所说的内容感到困惑。在我的上面的HTML中,我不关心子级<img>发生了什么。难道传播不只是到<body>吗? - tim peterson
@timpeterson:事件传播和事件委托不是同一回事。 - Blender
-@Blender 我知道它们不同,但是我的.ajaxLink点击事件会一直冒泡到<body>吗?是什么定义了它停止冒泡的位置? - tim peterson
显示剩余6条评论

0

如果可能的话,请勿使用stopPropagation()

使用stopPropagation()有两个优点:

  • 编写代码更容易(独立事件函数)
  • 性能

尽管此功能似乎很有用,但它可能被认为是不良的编码风格,特别是当您无法完全控制代码时(例如,因为您使用第三方库)。 stopPropagation()是一个全取或全不取的概念。它不允许精细的控制流程。如果在两个嵌套元素之间的某个元素停止了事件传播,则父元素中的任何一个都将无法接收到它,尽管可能存在应该接收它的情况。

解决这个问题更优雅(而且不那么复杂)的方法是始终允许事件传播,从不调用stopPropagation()。定义自己的事件并不希望从子元素自动执行的元素,可以使用targetcurrentTarget属性来检查初始事件的来源,并仅在需要时执行自己的事件函数。
在下面的示例中,有3个嵌套的DIV。单击最低的(蓝色)DIV将通过整个DOM结构向上传播onClick事件,但不调用位于其中的绿色DIV的onClick事件:
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <style>
            div {
                min-width: 100px;
                min-height: 50px;
                display: inline-block;
                padding: 2em;
            }
            #div1 { background: red; }
            #div2 { background: green; }
            #div3 { background: blue; }
            #info { display: block; white-space: pre; }
        </style>
        <script>
            window.addEventListener('load', function() {

                // #div1, #div3
                document.querySelector('#div1').addEventListener('click', function(e) {
                    if (e.target == e.currentTarget ||
                        e.target == document.querySelector('#div3')) {
                        document.querySelector('#info').textContent +=
                            'I am #1 or #3\n';
                    }
                });

                // #div2
                document.querySelector('#div2').addEventListener('click', function(e) {
                    if (e.currentTarget == e.target) {
                        document.querySelector('#info').textContent += 'I am #2\n';
                    }
                });
            });
        </script>
    </head>
    <body>
        <div id="div1">
            <div id="div2">
                <div id="div3"></div>
            </div>
        </div>
        <div id="info"></div>
    </body>
</html>

因此,在 DIV #3 的 onClick 事件函数中调用 stopPropagation() 并不是必需的,以防止触发 DIV #2 的 onClick 事件。

另外,请注意,文档结构如下:

document
  document.documentElement
    document.body
      ...

如果事件传播没有被停止,它将到达document对象。此时event.currentTarget将是document,而event.target将是document.documentElementdocument.body或者是<body>元素下的任何子元素。
因此,考虑以下代码:
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <style>
            html {
                background: #009688;
            }
            body {
                background: #bbb;
            }
        </style>
    </head>
    <body>
        <div>Hello world</div>
        <div>Hello world</div>
        <div>Hello world</div>
        <div>Hello world</div>
        <div>Hello world</div>
        <div>Hello world</div>
    </body>
</html>

这是它的外观以及文档中不同部分的位置: Simple page

灰色是主体区域。绿色是实际的文档“元素”(最顶层可样式化部分)。在其后面,有不可见的 document 对象。

如果您想要使用事件函数,这些函数仅将为直接在您手指/鼠标光标下的元素执行,您可以使用以下代码(针对 onClick 事件的示例):

elm.addEventListener('click', function(e) {
    if
    (
        (
            (e.currentTarget == document) &&
            (e.target == document.documentElement || e.target == document.body)
        )
        ||
        (e.currentTarget == e.target)
    )
    {
        // ...
    }
});

它适用于 document.documentElementdocument.body 或文档中的任何元素,无需调用 stopPropagation()


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