Skip to content

事件传播

元信息

  • 目标:回答"点在内层元素上,外层算不算被点"——事件在树上按捕获→目标→冒泡三阶段流动;能据此写出事件委托、模态框外点关闭等模式,并正确使用 stopPropagation/preventDefault
  • 关键概念:捕获阶段、目标阶段、冒泡阶段、事件委托、stopPropagation、stopImmediatePropagation、preventDefault
  • 关联阶段:cp3
  • 常见误区:以为点击只到达被点的那个元素(祖先会依次收到事件);混淆 preventDefault 与 stopPropagation(一个拦默认行为、一个拦传播);给每个子元素重复绑定监听(委托到父级即可,还能覆盖动态元素)

问题

  1. 当你指向一张海报,你指的是整个海报呢?还是海报上的明星呢?还是明星掌中的手机呢?
  2. 同样,当用户点击屏幕上的一块区域,那么是位于该区域中心的元素收到点击事件?还是代表整个区域的元素收到事件呢?还是整个网页呢?

浏览器的答案:我搞不清你究竟想点击谁,所以两种方案全提供

事件传播的三个阶段

DOM事件传播包含三个阶段,按以下顺序进行:

  1. 捕获阶段(Capturing Phase)
  2. 目标阶段(Target Phase)
  3. 冒泡阶段(Bubbling Phase)

事件冒泡

事件冒泡是指事件从最具体的元素(目标元素)开始,逐级向上传播到不太具体的元素(文档)。这是事件的默认传播方式。

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

当用户点击内层div时,事件传播顺序如下:

plain
内层div → 中层div → 外层div → body → html → document

冒泡示例代码

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

事件捕获

事件捕获与冒泡相反,事件从最不具体的元素(document)开始,逐级向下传播到最具体的元素(目标元素)。

基于上述使用相同的HTML结构,捕获阶段的传播顺序如下:

plain
document → html → body → 外层div → 中层div → 内层div

捕获示例代码

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

实际应用示例

示例1:事件委托

事件委托是利用事件冒泡机制的典型应用,可以在父元素上监听子元素的事件:

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>
import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

示例2:模态框点击外部关闭

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>
import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

阻止事件传播

stopPropagation()

event.stopPropagation() 方法可以阻止事件继续传播:

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

stopImmediatePropagation()

event.stopImmediatePropagation() 不仅阻止事件传播,还阻止同一元素上的其他事件监听器执行:

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

preventDefault()

event.preventDefault() 阻止事件的默认行为,但不阻止事件传播:

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;


常见问题与解决方案

问题1:事件监听器重复触发

问题描述:同一个元素被多次添加事件监听器,导致事件被多次处理。

解决方案

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

问题2:动态添加的元素无法响应事件

问题描述:通过JavaScript动态添加的元素,之前绑定的事件监听器无效。

解决方案:使用事件委托

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

问题3:移动端触摸事件与点击事件冲突

问题描述:在移动设备上,触摸事件和点击事件可能同时触发。

解决方案

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

总结

关键概念回顾

  1. 事件冒泡:事件从目标元素向上传播到文档根元素
  2. 事件捕获:事件从文档根元素向下传播到目标元素
  3. 事件委托:利用冒泡机制在父元素上处理子元素事件
  4. 阻止传播:使用stopPropagation()stopImmediatePropagation()

最佳实践

  1. 优先使用冒泡:大多数情况下,冒泡阶段更符合用户预期
  2. 合理使用事件委托:对于大量相似元素,使用事件委托可以提高性能
  3. 谨慎阻止事件传播:只在必要时阻止事件传播,避免影响其他功能
  4. 注意移动端兼容性:在移动设备上注意触摸事件与点击事件的区别

实践练习:交互式演示

创建一个HTML页面,包含嵌套的div元素,添加事件监听器,学习者可以直观地看到事件冒泡和捕获的过程。

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

扩展阅读