一、事件流模型

1. 事件流简介

事件流描述 DOM 事件 在 DOM 树中传播的完整过程,包含三个阶段:

2. 事件流的三大阶段

(1)捕获阶段(Capturing Phase)

  • 事件从 window目标元素 传播
  • 从最外层祖先元素向内传播,直到目标元素
  • 通过 addEventListener 的第三个参数为 true 启用

(2)目标阶段(Target Phase)

  • 事件到达实际触发事件的元素
  • 执行目标元素上绑定的所有事件监听器
  • 此阶段不分捕获和冒泡,按添加顺序执行

(3)冒泡阶段(Bubbling Phase)

  • 事件从 目标元素window 传播
  • 从目标元素向外传播到最外层祖先元素
  • 默认阶段,大多数事件在此阶段触发

3. 完整事件流顺序

1
2
3
4
5
6
7
8
9
10
11
用户交互触发事件

创建事件对象(包含所有事件信息)

捕获阶段:window → document → body → 父元素 → 目标元素
↓(依次执行每个元素上的捕获回调)
目标阶段:在目标元素上执行所有监听器
↓(按添加顺序执行,不分捕获/冒泡)
冒泡阶段:目标元素 → 父元素 → body → document → window
↓(依次执行每个元素上的冒泡回调)
事件结束

二、事件监听与注册

1. 事件监听方法

1
2
3
4
5
6
7
8
9
// 捕获阶段触发
element.addEventListener("click", callback, true);

// 冒泡阶段触发(默认)
element.addEventListener("click", callback, false);
element.addEventListener("click", callback); // 简写形式

// 移除事件监听
element.removeEventListener("click", callback, useCapture);

2. 事件传播控制

1
2
3
4
element.addEventListener("click", function (event) {
event.stopPropagation(); // 阻止事件继续传播
event.stopImmediatePropagation(); // 阻止传播并阻止同一元素的其他监听器
});

三、事件对象(Event Object)

1. 事件对象基础

  • 事件触发时浏览器自动创建
  • 作为第一个参数传递给所有事件监听器
  • 包含事件的完整上下文信息
  • 在整个事件流中传递的是同一个对象

2. 核心属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 目标元素相关
event.target; // 实际触发事件的元素(永远不变)
event.currentTarget; // 当前正在处理事件的元素(动态变化)
event.relatedTarget; // 相关元素(如 mouseover 时来自的元素)

// 事件信息
event.type; // 事件类型:'click', 'keydown'
event.timeStamp; // 事件发生时间戳
event.bubbles; // 是否冒泡
event.cancelable; // 能否取消默认行为
event.defaultPrevented; // 是否已阻止默认行为

// 事件阶段
event.eventPhase; // 事件阶段:1-捕获, 2-目标, 3-冒泡

3. 事件对象方法

1
2
3
event.preventDefault(); // 阻止默认行为
event.stopPropagation(); // 阻止事件继续传播
event.stopImmediatePropagation(); // 阻止传播并阻止其他监听器

四、不同类型事件的特殊属性

1. 鼠标事件(MouseEvent)

1
2
3
4
5
6
7
8
9
10
11
12
// 坐标信息
event.clientX, event.clientY; // 相对于视口
event.pageX, event.pageY; // 相对于文档
event.screenX, event.screenY; // 相对于屏幕
event.offsetX, event.offsetY; // 相对于目标元素

// 鼠标状态
event.button; // 0-左键, 1-中键, 2-右键
event.buttons; // 按下的按钮掩码

// 修饰键
event.altKey, event.ctrlKey, event.shiftKey, event.metaKey;

2. 键盘事件(KeyboardEvent)

1
2
3
4
5
event.key; // 按键名:'a', 'Enter'
event.code; // 物理键码:'KeyA', 'Enter'
event.keyCode; // 已废弃的键码
event.altKey, event.ctrlKey, event.shiftKey, event.metaKey;
event.repeat; // 是否重复按键

3. 拖拽事件(DragEvent)

1
2
event.dataTransfer; // 数据传递对象
event.dataTransfer.files; // 拖拽的文件列表

五、事件执行顺序详解

1. 同一元素的多个监听器

1
2
3
4
5
element.addEventListener("click", () => console.log("第一个"));
element.addEventListener("click", () => console.log("第二个"));

// 输出:第一个 → 第二个
// 严格按照添加顺序执行

2. 完整事件流示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="outer">
<div id="inner">
<button id="btn">Click</button>
</div>
</div>

<script>
// 捕获阶段(从外向内)
outer.addEventListener("click", () => console.log("outer 捕获"), true);
inner.addEventListener("click", () => console.log("inner 捕获"), true);

// 目标阶段
btn.addEventListener("click", () => console.log("btn 目标1"));
btn.addEventListener("click", () => console.log("btn 目标2"));

// 冒泡阶段(从内向外)
inner.addEventListener("click", () => console.log("inner 冒泡"), false);
outer.addEventListener("click", () => console.log("outer 冒泡"), false);
</script>

点击输出:

1
2
3
4
5
6
outer 捕获
inner 捕获
btn 目标1
btn 目标2
inner 冒泡
outer 冒泡

六、事件委托(Event Delegation)

1. 事件委托原理

利用事件冒泡,在父元素上处理子元素的事件

2. 事件委托优势

  • 减少事件监听器数量
  • 动态添加的子元素自动拥有事件处理
  • 更好的性能表现

3. 事件委托实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<ul id="itemList">
<li data-id="1">项目1</li>
<li data-id="2">项目2</li>
<li data-id="3">项目3</li>
</ul>

<script>
document
.getElementById("itemList")
.addEventListener("click", function (event) {
// 检查实际点击的元素
if (event.target.tagName === "LI") {
const itemId = event.target.dataset.id;
console.log("点击了项目:", itemId);
}
});
</script>

七、自定义事件

1. 创建自定义事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建自定义事件
const customEvent = new CustomEvent("myEvent", {
detail: {
message: "自定义数据",
timestamp: Date.now(),
},
bubbles: true,
cancelable: true,
});

// 触发自定义事件
element.dispatchEvent(customEvent);

// 监听自定义事件
element.addEventListener("myEvent", function (event) {
console.log("自定义事件数据:", event.detail);
});

八、事件对象生命周期

1. 创建阶段

1
2
3
4
5
6
7
// 事件触发时一次性创建
const event = new MouseEvent("click", {
target: buttonElement,
clientX: 100,
clientY: 200,
// 其他初始属性...
});

2. 传播过程中的变化

1
2
3
4
5
6
7
8
9
// 动态变化的属性
event.currentTarget; // 当前处理元素(随传播变化)
event.eventPhase; // 事件阶段(1→2→3变化)

// 固定不变的属性
event.target; // 原始触发元素(不变)
event.type; // 事件类型(不变)
event.timeStamp; // 时间戳(不变)
// 大多数其他属性不变

九、实用模式和最佳实践

1. 事件调试工具

1
2
3
4
5
6
7
8
function debugEvent(event) {
console.group("事件调试");
console.log("类型:", event.type);
console.log("目标:", event.target);
console.log("当前目标:", event.currentTarget);
console.log("阶段:", event.eventPhase);
console.groupEnd();
}

2. 事件委托最佳实践

1
2
3
4
5
6
7
// 使用 closest 方法提高容错性
document.getElementById("list").addEventListener("click", function (event) {
const listItem = event.target.closest("li.item");
if (listItem) {
console.log("点击项目:", listItem.dataset.id);
}
});

3. 性能优化

1
2
3
4
5
// 使用 passive 事件监听器提高滚动性能
element.addEventListener("touchstart", handler, { passive: true });

// 使用 once 选项让事件只执行一次
element.addEventListener("click", handler, { once: true });

十、核心要点总结

1. 事件流要点

  • 三个阶段:捕获 → 目标 → 冒泡
  • 执行顺序:每个元素上的回调按添加顺序依次执行
  • 传播控制stopPropagation()stopImmediatePropagation()

2. 事件对象要点

  • 统一对象:整个事件流使用同一个事件对象
  • 目标区分target(实际触发)vs currentTarget(当前处理)
  • 行为控制:阻止默认行为、停止传播等方法

3. 最佳实践要点

  • 事件委托:处理动态内容,减少事件绑定
  • 性能优化:使用 passive、once 等选项
  • 错误处理:使用 closest 等方法提高容错性

4. 重要概念

  • 事件对象在触发时创建,传播过程中部分属性被修改
  • 同一元素的多个监听器按添加顺序执行
  • 事件委托利用冒泡机制简化事件处理
  • 理解事件流顺序对于编写可预测的代码至关重要