个人博客:haichenyi.com。感谢关注
一. 目录
二. 核心原理
什么是事件的委托? 事件委托就是利用冒泡机制,把子元素的事件统一委托给父元素处理的优化机制。在部分场景,能显著提升性能。
- 事件冒泡:当子元素触发事件(如:点击),会统一从子元素从下往上传递,一直传递到document
- 父元素统一监听:通过给父元素绑定事件监听器,利用event.target识别实际触发的子元素,避免为每个子元素添加事件。
以上就有两个问题:什么是事件冒泡?怎么给父元素绑定监听器?
三. 事件的组成
JavaScript中一个事件是由三个阶段组成:捕获阶段,目标阶段,冒泡阶段。
阶段 | 触发顺序 | 监听方式 |
---|---|---|
捕获阶段 | 1 | addEventListener(type, handler, true) |
目标阶段 | 2 | 直接绑定到目标元素的事件 |
冒泡阶段 | 3 | addEventListener(type, handler, false) |
举个栗子:
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>事件委托</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<style>
.grandparent {
width: 200px;
height: 200px;
background-color: aqua;
}
.parent {
width: 100px;
height: 100px;
background-color: blanchedalmond;
}
.child {
width: 50px;
height: 50px;
background-color: brown;
}
</style>
</head>
<body>
<div id="grandparent" class="grandparent">
<div id="parent" class="parent">
<button id="child" class="child"></button>
</div>
</div>
</body>
<script>
//捕获阶段监听
document.getElementById('grandparent').addEventListener('click', (event) => {
console.log("grandparent 捕获阶段");
}, true)
document.getElementById('parent').addEventListener('click', (event) => {
console.log("parent 捕获阶段");
}, true)
document.getElementById('child').addEventListener('click', (event) => {
console.log("child 捕获");
}, true)
//冒泡阶段监听
document.getElementById('child').addEventListener('click', (event) => {
console.log("child 冒泡");
}, false)
document.getElementById('parent').addEventListener('click', (event) => {
console.log("parent 冒泡阶段");
}, false)
document.getElementById('grandparent').addEventListener('click', (event) => {
console.log("grandparent 冒泡阶段");
}, false)
</script>
</html>
上面这个简单的例子的就是一个事件的整个执行过程。三张图分别点击三个区域的打印
一共三个元素,分别监听了他们的click事件。
- 当我点击child的时候,我的目标元素就是child。会先执行:grandparent(捕获)—>parent(捕获)—>child(捕获)—>child冒泡—>parent(冒泡)—>grandparent(冒泡)
- 当我点击parent的时候,我的目标元素就是parent。执行顺序:grandparent(捕获)—>parent(捕获)—>parent(冒泡)—>grandparent(冒泡)
- 当我点击grandparent的时候,我们的目标元素就是grandparent。执行顺序:grandparent(捕获)—>grandparent(冒泡)
有人就会问了,三个阶段,为什么没有目标阶段?什么是目标阶段?表格上面就说了。目标阶段直接绑定在目标元素上。 可以这么理解。当我点击child元素的时候,当事件传递到child身上的时候,目标阶段就已经触发了。目标阶段是一个独立的阶段,捕获和冒泡是事件传递的路径。
说白了,就是Android里面的事件触摸的传递。down事件:由activity到目标view,up事件:由view到activity的过程。return true还是false,表示是否需要自己消费整个事件,不继续传递。
捕获对应down事件,冒泡对应up事件。
类型 | 向下事件 | 向上事件 | 是否自己消费 |
---|---|---|---|
Android | Down | UP | return true/false |
JavaScript | 捕获阶段 | 冒泡阶段 | stopPropagation |
当父子view都添加了点击事件,当我触发子view的点击事件的时候,不想触发父view的点击事件,该怎么做呢?阻止事件冒泡,也就是上面的说,自己消费这个事件,不让它继续传递了。比如:
document.getElementById('child').addEventListener('click', (event) => {
console.log("child 冒泡阶段");
//直接给目标view,添加一行这个代码,表示组织事件冒泡,也就是不继续传递了。
event.stopPropagation()
}, false)
特殊的事件
有一些事件是没有冒泡事件的
事件类型 | 解决方案 |
---|---|
focus/blur | 使用 focusin/focusout(支持冒泡) |
mouseenter/mouseleave | 改用 mouseover/mouseout + 条件判断 |
load/unload | 仅作用于目标元素 |
四. 如何实现
我们开头举的例子使用事件委托,需要怎么改呢?
document.getElementById('grandparent').addEventListener('click', (event) => {
// 识别实际点击的目标元素
const target = event.target.closest('#child, #parent, #grandparent');
if (!target) return; // 非目标元素则退出
// 根据元素 ID 执行不同逻辑
switch (target.id) {
case 'child':
console.log('委托处理:child 被点击');
// event.stopPropagation(); // 如需阻止冒泡在此处调用
break;
case 'parent':
console.log('委托处理:parent 被点击');
break;
case 'grandparent':
console.log('委托处理:grandparent 被点击');
break;
}
});
如上的代码,我们给grandparent设置点击事件,然后利用event对象,判断点击的组件时哪个,就触发对应的逻辑即可。就是这么简单。
核心思想,就是找到你想要设置点击事件view,他们功能的父view,然后给父view设置点击事件,利用事件分发的原理,判断即可。
这里有几点需要注意:
属性 | 说明 |
---|---|
event.target | 始终指向实际触发事件的view |
event.currentTarget | 指向当前绑定监听事件的view(在本例中也就是上面说的grandparent) |
方法 | 作用范围 | 说明 |
---|---|---|
event.stopPropagation() | 当前阶段 | 阻止事件继续传播(不影响同阶段其他监听器) |
event.stopImmediatePropagation() | 所有阶段 | 立即停止所有后续监听器执行 |
五. 高频使用场景
动态列表高频更新
当 v-for 渲染的列表频繁增删时,单独绑定事件会导致重复解绑/绑定。
优化方案: 委托到父容器,利用 event.target 判断触发元素
<div class="list-container" @click="handleDelegateClick">
<div v-for="item in dynamicList" :data-id="item.id">{{ item.text }}</div>
</div>
methods: {
handleDelegateClick(event) {
const target = event.target.closest('[data-id]');
if (!target) return;
const itemId = target.dataset.id;
// 根据 id 找到对应数据
}
}
复杂子元素结构
当子元素内部包含交互元素时,直接 @click 会导致事件目标判断困难:
<div class="card" @click="handleCard">
<button @click.stop="handleDelete">删除</button>
</div>
问题: 点击按钮会同时触发 handleCard 和 handleDelete
解决方案: 统一委托到父级,通过类名区分目标
<div class="cards-container" @click="handleContainerClick">
<div class="card" data-type="card">
<button data-type="deleteBtn">删除</button>
</div>
</div>
methods: {
handleContainerClick(event) {
const type = event.target.dataset.type;
switch(type) {
case 'card':
// 处理卡片点击
break;
case 'deleteBtn':
// 处理删除按钮
break;
}
}
}
超长列表性能优化
渲染 1000+ 个带点击事件的元素时,单独绑定会导致内存激增。
绑定方式 | 内存占用 | 滚动流畅度 |
---|---|---|
单独 @click | 高 | 卡顿 |
事件委托 | 低 | 流畅 |
ps:并不是所有的列表都需要,列表足够长才需要。一般不需要。
六. 最佳实践指南
场景 | 推荐方案 |
---|---|
小型静态列表 (<100 项) | 直接使用 @click |
动态增删列表 | 父级容器委托 + dataset 标记 |
高频交互的可滚动长列表 | 虚拟滚动 + 事件委托 |
需要精确控制事件传播的场景 | 委托 + event.stopPropagation |
决策树:
元素数量 > 500? → 是 → 必须用事件委托
↓ 否
是否需要动态更新? → 是 → 推荐委托
↓ 否
是否有复杂子结构? → 是 → 建议委托
↓ 否
直接使用 @click
PS:整篇文章,精炼一下,如下
- 事件委托的核心原理:利用冒泡机制和事件统一交给父view处理
- 事件的组成三个阶段:捕获,目标,冒泡
- 捕获阶段:从上往下传递,冒泡阶段冲下往上传递。
- 怎么实现事件委托:父view统一监听,使用event.target属性去判断。使用event.stopPropagation()阻止事件传递
- 什么情况下需要使用事件委托?