Vue 3:我在真实项目中如何用事件委托
上一篇冒泡带了一嘴事件循环,这一篇就来讲讲吧
在开发项目的时候,我其实很早就知道"事件冒泡""事件委托"这些概念,但真正让我对事件委托产生足够深刻理解,是在做一个动态的 Todo 列表模块时遇到的问题。当时我给每个列表项里的删除按钮都写了 @click="deleteItem(item.id)",结果列表一多、结构一复杂,我就发现维护变得越来越吃力。
后来我重构成事件委托,一下子清爽了许多,也顺便把事件冒泡、事件修饰符等机制彻底吃透了。所以,我在这篇文章里,把我真实的开发体验、踩过的坑、以及最后怎么通过事件委托解决问题,都记录下来。
一、我最开始的写法(未使用事件委托)
这是大部分初学者最熟悉的写法,也确实能工作:
html
<ul ref="todoList" class="todo-list">
<li v-for="item in todos" :key="item.id" :data-id="item.id">
<span>{{ item.text }}</span>
<!-- 每个按钮都独立绑定事件 -->
<button class="delete" @click="deleteItem(item.id)">删除</button>
</li>
</ul>
这个写法很直观,但当时我遇到了三个问题:
1. 列表稍微大一点,事件监听器就变多
一百条数据就有一百个 click 事件监听。Vue 会帮我做虚拟 DOM diff,但事件监听器还是被绑定到真实 DOM 上的。
2. 列表是动态的,每次增删都会重新创建 / 销毁事件监听器
我当时的 Todo 列表支持"从服务器滚动加载更多",每次追加数据就会追加更多按钮。
3. 每一个按钮都写 @click,模板开始变得臃肿
特别是当一个列表项里不止一个按钮(编辑、复制、删除、更多)的时候,模板根本不想看。
这些问题叠加起来,促使我开始考虑事件委托。
二、重构后的写法:使用事件委托
改用事件委托后,我只在父元素 <ul> 上写一个 @click:
html
<ul ref="todoList" class="todo-list" @click="onListClick">
<li v-for="item in todos" :key="item.id" :data-id="item.id">
<span>{{ item.text }}</span>
<!-- 删除按钮不再绑定事件 -->
<button class="delete">删除</button>
</li>
</ul>
然后在逻辑里统一处理:
js
function onListClick(event) {
const target = event.target
if (target.classList.contains('delete')) {
const li = target.closest('li')
const id = li.dataset.id
deleteItem(id)
}
}
改完以后,我当时立刻感受到模板轻松了许多,而且新增数据、删除数据完全不需要我关心事件绑定的问题了,全自动生效。
三、事件委托在 Vue 3 中依然必要吗?我真实的结论:必要,而且很好用
有人认为:既然 Vue 有虚拟 DOM,有组件系统,那是不是事件委托就没必要了?
但我真实项目经验告诉我:事件委托依然非常有价值。
1. Vue 事件绑定再智能,也无法帮你减少真实 DOM 的监听器数
虚拟 DOM 再聪明,真实 DOM 上有多少监听器,那是实打实的。
事件委托减少的是浏览器层面的监听器数量,Vue 无法替你做这个优化。
2. 动态列表(频繁增删)时,委托是最稳的方案
我当时的列表是"不断加载更多",如果用子节点绑定事件:
- 每次新增数据 → 新增监听器
- 每次删除数据 → 删除监听器
委托只需要一个监听器。
3. 多个按钮、多种操作时,委托逻辑比模板绑定更清晰
如果有:编辑 / 复制 / 分享 / 删除 ...
模板这样写真的很乱:
html
<button @click="edit(item)">编辑</button>
<button @click="copy(item)">复制</button>
<button @click="share(item)">分享</button>
<button @click="delete(item)">删除</button>
但委托只需要一个判断:
js
if (target.classList.contains('edit')) { ... }
if (target.classList.contains('copy')) { ... }
if (target.classList.contains('share')) { ... }
if (target.classList.contains('delete')) { ... }
这在复杂交互中特别爽。
四、顺带讲讲:事件捕获 / 冒泡 / 修饰符(.stop .prevent .self)到底什么时候用?
在写事件委托时,我顺便把 Vue 的事件修饰符用得很熟,这里总结一下。
1)事件冒泡(bubbling)
事件从目标元素向上冒泡到父元素、再到根。
事件委托正是利用了冒泡。
2)事件捕获(capturing)
事件从外往内传播(一般很少用)。
Vue 里可用:
ini
@click.capture="handle"
但除非做特殊行为,一般不用。
3).stop ------ 阻止冒泡
js
@click.stop="deleteItem"
这会阻止事件继续冒泡到 <ul>。
在事件委托中,有些按钮可能不希望触发 onListClick,就可以用它。
4).prevent ------ 阻止默认行为
如阻止 <a> 的跳转:
js
@click.prevent="openModal"
5).self ------ 仅当点到自身时触发
js
@click.self="closeDialog"
不会因为点击子元素而触发。
这个我在 dialog 遮罩层里用过。
五、我的最终感受
事件委托是一个看似简单却非常实用的技巧:
- 模板更干净
- 父级统一管理事件更好维护
- 动态列表也无需重新绑定事件
- 性能更轻量
在 Vue 3 里,它依然能帮你写出更优雅也更专业的代码。
如果你的项目里也有大量列表、按钮、表格类结构,我非常建议你尝试一次事件委托 ------ 你可能会像我一样,写完之后再也回不去过去那种"按钮上绑一堆事件"的方式了。