事件冒泡和捕获是什么?

1243

事件冒泡和捕获之间有什么区别?何时应该使用冒泡而不是捕获?


8
我推荐这个有用的链接:https://javascript.info/bubbling-and-capturing。 - MeirDayan
@CommunityAns:这个网站非常棒,但是这个主题特别的表述有点混乱。 - Veverke
1
需要工程师们意识到两者之间的差异,并且编写的代码可以通过选择“不正确”的传播类型轻松破坏,这在我看来是一种代码异味(或反模式)。更好的方法是只强制执行一种编码风格,其中传播类型并不重要。 - Chris Vilches
11个回答

1744
事件冒泡和捕获是HTML DOM API中事件传播的两种方式,当元素内部的一个元素触发事件并且这两个元素都注册了该事件的处理函数时,事件传播模式决定元素接收事件的顺序
使用冒泡,事件首先被最内层的元素捕获并处理,然后传播到外层元素。
使用捕获,事件首先被最外层的元素捕获,然后传播到内层元素。
捕获也称为“滴落”,有助于记住传播顺序:

自上而下,自下而上

早期,Netscape倡导事件捕获,而Microsoft则推广事件冒泡。两者都是W3C 文档对象模型事件标准(2000)的一部分。
IE<9仅使用事件冒泡,而IE9+和所有主要浏览器都支持两种方式。另一方面,对于复杂的DOM,事件冒泡的性能可能稍低
我们可以使用addEventListener(type, listener, useCapture)在冒泡(默认)或捕获模式下注册事件处理程序。要使用捕获模式,请将第三个参数设置为true

示例

<div>
    <ul>
        <li></li>
    </ul>
</div>

在上面的结构中,假设在li元素中发生了点击事件。
在捕获模型中,事件将首先由div处理(div中的点击事件处理程序将首先触发),然后在ul中处理,最后在目标元素li中处理。
在冒泡模型中,相反会发生:事件将首先由li处理,然后由ul处理,最后由div元素处理。
有关更多信息,请参见: 在下面的示例中,如果单击任何突出显示的元素,您可以看到事件传播流程的捕获阶段首先发生,然后是冒泡阶段。

var logElement = document.getElementById('log');

function log(msg) {
    logElement.innerHTML += ('<p>' + msg + '</p>');
}

function capture() {
    log('capture: ' + this.firstChild.nodeValue.trim());
}

function bubble() {
    log('bubble: ' + this.firstChild.nodeValue.trim());
}

function clearOutput() {
    logElement.innerHTML = "";
}

var divs = document.getElementsByTagName('div');
for (var i = 0; i < divs.length; i++) {
    divs[i].addEventListener('click', capture, true);
    divs[i].addEventListener('click', bubble, false);
}
var clearButton = document.getElementById('clear');
clearButton.addEventListener('click', clearOutput);
p {
    line-height: 0;
}

div {
    display:inline-block;
    padding: 5px;

    background: #fff;
    border: 1px solid #aaa;
    cursor: pointer;
}

div:hover {
    border: 1px solid #faa;
    background: #fdd;
}
<div>1
    <div>2
        <div>3
            <div>4
                <div>5</div>
            </div>
        </div>
    </div>
</div>
<button id="clear">clear output</button>
<section id="log"></section>

在JSFiddle上的另一个示例


45
useCapture现在在IE >= 9中得到支持。来源 - beatgammit
8
我知道留评论已经太晚了,但我在这里发现了一篇不错的文章。 http://catcode.com/domcontent/events/capture.html - Just code
4
“trickling”和“capturing”是相同的吗? Crockford在这个视频讲话中谈到了“Trickling v. Bubbling” - https://www.youtube.com/watch?v=Fv9qT9joc0M&list=PL7664379246A246CB 大约在1小时5分钟左右。 - Kevin Meredith
12
以上回答在详细解释中关于顺序的部分是正确的,但会让你认为“泡沫上升、向下滴漏”中的滴漏发生在第二个阶段。事件总是在冒泡阶段之前经历捕获阶段。 正确的顺序是向下滴漏 => onElement => 泡沫上升 - runspired
2
通过冒泡,事件首先被最内层元素捕获和处理,然后传播到外部元素。需要指出并非所有事件都会冒泡(例如 focus)。 - thdoan
显示剩余9条评论

573

Description:

quirksmode.org 提供了一个很好的描述。简而言之(从quirksmode复制):

事件捕获

当您使用事件捕获时

               | |
---------------| |-----------------
| element1     | |                |
|   -----------| |-----------     |
|   |element2  \ /          |     |
|   -------------------------     |
|        Event CAPTURING          |
-----------------------------------

元素1的事件处理程序首先触发,元素2的事件处理程序最后触发。

事件冒泡

当您使用事件冒泡时

               / \
---------------| |-----------------
| element1     | |                |
|   -----------| |-----------     |
|   |element2  | |          |     |
|   -------------------------     |
|        Event BUBBLING           |
-----------------------------------

元素2的事件处理程序首先触发,元素1的事件处理程序最后触发。

要使用什么?

这取决于您想要做什么。没有更好的选择。差异在于事件处理程序执行的顺序。大多数情况下,在冒泡阶段触发事件处理程序是可以接受的,但有时需要更早地触发它们。


1
不会同时发生捕获和冒泡,那么什么是事件分派? - Suraj Jain
1
一个图形化的例子在这里:https://javascript.info/bubbling-and-capturing - MeirDayan
1
当某个元素的代码(例如插件等你无法控制的代码)停止事件传播时,捕获阶段处理程序特别有用,因为你真正想知道事件何时发生。在到达它们的途中捕获事件,而不是在返回途中捕获事件,这样做可以帮助你避免在冒泡阶段无法获取事件,因为它们已经停止了事件的传播。 - doug65536

86
如果有两个元素element1和element2。Element2在element1内部,我们为这两个元素都绑定一个事件处理程序,比如onClick。现在当我们单击元素2时,两个元素的事件处理程序都将被执行。现在问题是事件将按照什么顺序执行。如果与element1绑定的事件先执行,则称为事件捕获;如果先执行与element2绑定的事件,则称为事件冒泡。
根据W3C规范,事件将从捕获阶段开始,直到达到目标,然后返回到元素并开始冒泡。
addEventListener方法的useCapture参数可以确定捕获和冒泡状态。

eventTarget.addEventListener(type,listener,[,useCapture]);

默认情况下,useCapture为false,即处于冒泡阶段。
var div1 = document.querySelector("#div1");
var div2 = document.querySelector("#div2");

div1.addEventListener("click", function (event) {
  alert("you clicked on div 1");
}, true);

div2.addEventListener("click", function (event) {
  alert("you clicked on div 2");
}, false);
#div1{
  background-color:red;
  padding: 24px;
}

#div2{
  background-color:green;
}
<div id="div1">
  div 1
  <div id="div2">
    div 2
  </div>
</div>

请尝试更改true和false。


3
@masterxilo:不需要Fiddle了,现在StackOverflow支持内联代码(stack snippets) - Dan Dascalescu
关于“事件从捕获阶段开始,直到达到目标元素并返回到元素,然后开始冒泡”的内容。我只发现addEventListener有一个参数useCapture,可以设置为true或false;而在HTML 4.0中,事件侦听器被指定为元素的属性,并且useCapture默认为false。您能否提供一个链接到确认您所写内容的规范? - surfmuggle

38

我发现这个javascript.info的教程在解释这个主题时非常清晰。它在结尾处的三点总结真正涉及到了关键点。我在这里引用:

  1. 事件首先被捕获到最深的目标,然后冒泡上升。在IE<9中,它们只会冒泡。
  2. 除了使用最后一个参数trueaddEventListener之外,所有处理程序都在冒泡阶段工作,这是唯一可以在捕获阶段捕获事件的方法。
  3. 冒泡/捕获可以通过event.cancelBubble=true(IE)或event.stopPropagation()(其他浏览器)停止。

14

DOM事件描述了事件传播的3个阶段:捕获阶段 - 事件从父元素向下传递到目标元素;目标阶段 - 事件达到目标元素;冒泡阶段 - 事件从目标元素向上冒泡。

在此输入图片描述


13

还有一个 Event.eventPhase 属性,可以告诉你事件是在目标阶段还是来自其他地方,并且它得到了浏览器的完全支持。

基于已经接受答案中的精彩代码片段,这是使用eventPhase属性的输出结果。

var logElement = document.getElementById('log');

function log(msg) {
  if (logElement.innerHTML == "<p>No logs</p>")
    logElement.innerHTML = "";
  logElement.innerHTML += ('<p>' + msg + '</p>');
}

function humanizeEvent(eventPhase){
  switch(eventPhase){
    case 1: //Event.CAPTURING_PHASE
      return "Event is being propagated through the target's ancestor objects";
    case 2: //Event.AT_TARGET
      return "The event has arrived at the event's target";
    case 3: //Event.BUBBLING_PHASE
      return "The event is propagating back up through the target's ancestors in reverse order";
  }
}
function capture(e) {
  log('capture: ' + this.firstChild.nodeValue.trim() + "; " + 
  humanizeEvent(e.eventPhase));
}

function bubble(e) {
  log('bubble: ' + this.firstChild.nodeValue.trim() + "; " + 
  humanizeEvent(e.eventPhase));
}

var divs = document.getElementsByTagName('div');
for (var i = 0; i < divs.length; i++) {
  divs[i].addEventListener('click', capture, true);
  divs[i].addEventListener('click', bubble, false);
}
p {
  line-height: 0;
}

div {
  display:inline-block;
  padding: 5px;

  background: #fff;
  border: 1px solid #aaa;
  cursor: pointer;
}

div:hover {
  border: 1px solid #faa;
  background: #fdd;
}
<div>1
  <div>2
    <div>3
      <div>4
        <div>5</div>
      </div>
    </div>
  </div>
</div>
<button onclick="document.getElementById('log').innerHTML = '<p>No logs</p>';">Clear logs</button>
<section id="log"></section>


MDN关于composedPath的相关文档以及DOM元素的阴影边界,是很好的附加上下文。 - New Alexandria

9

冒泡排序

  Event propagate to the upto root element is **BUBBLING**.

捕获

  Event propagate from body(root) element to eventTriggered Element is **CAPTURING**.

3
当浏览器检测到事件时,它会尝试查找事件处理程序。这个过程有三个阶段。假设我们有以下元素:
   <body>
      <div>
        <button>click</button>
      </div>
    </body>

1-捕获阶段

浏览器会查看刚刚点击的元素,然后它将转到最上层的父元素,即 body。如果在 body 中有任何点击处理程序,浏览器将调用它。在检查了 body 元素之后,它将查看第二个顶级父元素,即 div 元素。这个过程将重复,直到浏览器到达最底部的按钮元素。一旦它看到按钮元素,第一阶段 - 捕获就结束了。大多数情况下,我们忽略此阶段。此代码忽略此阶段。

document.addEventListener('click',handleClick)

如果您想参与此阶段,您需要编写此内容。
document.addEventListener('click',handleClick,true)

我们需要这个阶段来检测目标外部的点击。也许我们有一个下拉菜单或模态框打开,如果用户在模态框或下拉菜单以外的任何地方点击,我们想要关闭它。
2-目标阶段
浏览器将查看被点击的元素,即按钮,如果该按钮具有事件处理程序,则会调用它。
3-冒泡阶段
捕获阶段相反,在此阶段中,浏览器将从立即父元素开始处理,即在此情况下为div元素,然后它将访问body元素。此代码将为冒泡阶段设置事件处理程序。
document.addEventListener('click',handleClick,false)

3

我做了一个小例子,你可以在其中实时体验“事件冒泡”:https://codepen.io/abernier/pen/yKGJXK?editors=1010

enter image description here

点击任何 div,您将看到“click”事件冒泡!
$divs.forEach(($div) => $div.addEventListener("click", handleClick2));

2

正如其他人所说,冒泡和捕获描述了一些嵌套元素接收给定事件的顺序。

我想指出的是,对于最内层的元素可能会出现一些奇怪的情况。实际上,对于某些浏览器(例如Mozilla Firefox),添加事件监听器的顺序确实很重要。

在下面的示例中,div2的捕获将先于冒泡执行;而div4的冒泡将先于捕获执行。

function addClickListener (msg, num, type) {
  document.querySelector("#div" + num)
    .addEventListener("click", () => alert(msg + num), type);
}
bubble  = (num) => addClickListener("bubble ", num, false);
capture = (num) => addClickListener("capture ", num, true);

// first capture then bubble
capture(1);
capture(2);
bubble(2);
bubble(1);

// try reverse order
bubble(3);
bubble(4);
capture(4);
capture(3);
#div1, #div2, #div3, #div4 {
  border: solid 1px;
  padding: 3px;
  margin: 3px;
}
<div id="div1">
  div 1
  <div id="div2">
    div 2
  </div>
</div>
<div id="div3">
  div 3
  <div id="div4">
    div 4
  </div>
</div>

备注: 请尝试使用Mozilla Firefox运行上面的代码片段。


添加事件监听器的顺序不重要,如果还不确定,请尝试自己的示例。 - sasidhar
@sasidhar 我的例子已经很清楚了。现在,如果你点击 div #4,你会得到“capture 3, bubble 4, capture 4, bubble 3”。如果你反转顺序并断言 capture(3); capture(4); bubble(4); bubble(3);,然后再次点击 div #4,你会得到“capture 3, capture 4, bubble 4, bubble 3”。这是一个事实,尽管我无法解释它。 - logi-kal
1
在Edge和Chrome中尝试了你的例子,无论顺序如何,结果始终为capture(3); capture(4); bubble(4); bubble(3) - sasidhar
1
@sasidhar 抱歉回复晚了。我使用Firefox,不管怎样。 - logi-kal
@RahulJain Mozilla Firefox 是一款重要的浏览器。 - logi-kal
显示剩余2条评论

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