一、前言
004 篇初识了 @click,但这只算入门。真实项目里的事件处理有一堆讲究:怎么拿到事件对象、一个元素怎么同时处理多个事件、阻止冒泡和默认行为不想再手写 e.preventDefault() 怎么办。本篇把这些一次补齐,核心是让代码量砍一半的事件修饰符。
二、内联表达式与方法的边界
简单逻辑直接内联,超过一行就抽方法------这个边界要守住:
vue
<template>
<!-- ✅ 简单逻辑:内联一行搞定 -->
<button @click="count++">内联:{{ count }}</button>
<!-- ✅ 复杂逻辑:抽方法,可读性好 -->
<button @click="increment">方法:{{ count }}</button>
<!-- ❌ 内联塞一坨逻辑:能跑,但没人想维护 -->
<button @click="count++; if (count > 10) { count = 0 }; logged++">地狱写法</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const logged = ref(0)
function increment() {
count.value++
}
</script>
三、传参与 $event:既要传参又要事件对象
方法需要额外参数,同时还想要原生事件对象,用**$event** 占位:
vue
<template>
<!-- 不传参:默认收到事件对象 -->
<button @click="onClick">无参</button>
<!-- 传自定义参数 -->
<button @click="say('hi')">传参</button>
<!-- 既传参又要事件对象:$event 固定代表原生事件 -->
<button @click="say('hi', $event)">传参 + 事件</button>
</template>
<script setup>
function onClick(e) {
console.log('事件对象:', e.target.textContent)
}
function say(word, e) {
console.log(word, e && e.type) // hi click
}
</script>
记住:$event 就是原生事件对象的"专用座位",写在哪个参数位置就传到哪。
四、一个元素绑多个事件:数组写法
同一个事件挂多个处理函数,用数组包起来:
vue
<template>
<button @click="[handlerOne, handlerTwo]">一箭双雕</button>
</template>
<script setup>
function handlerOne() { console.log('第一个') }
function handlerTwo() { console.log('第二个') }
</script>
五、事件修饰符:告别手写 e.stopPropagation()
先看没有修饰符的原生写法多啰嗦:
js
function submit(e) {
e.preventDefault() // 阻止默认行为
e.stopPropagation() // 阻止冒泡
// ...业务代码
}
Vue 把这些高频操作做成"后缀糖",直接跟在 @ 后面:
| 修饰符 | 作用 | 记忆 |
|---|---|---|
| .stop | 阻止冒泡 | 别吵到我爸 |
| .prevent | 阻止默认行为 | a 不跳转、form 不提交 |
| .once | 只触发一次 | 一锤子买卖 |
| .self | 只有自己被点才触发 | 点我儿子不算点我 |
| .capture | 用捕获模式监听 | 先爸后子的反向流程 |
| .passive | 告诉浏览器不阻止默认行为 | 滚动性能优化 |
1. .stop------最常用,弹层场景
vue
<template>
<div class="mask" @click="close">
<div class="dialog" @click.stop>点这里不会关闭弹窗</div>
</div>
</template>
不加 .stop,点弹窗内部也会冒泡到遮罩导致关闭------加了就"隔断"。
2. .prevent------表单与链接
vue
<template>
<!-- 表单不刷新页面(配合后面 019 篇的提交) -->
<form @submit.prevent="onSubmit">
<input v-model="text" />
<button>提交</button>
</form>
<a href="https://vuejs.org" @click.prevent>链接失效了</a>
</template>
3. .once------只来一次
vue
<button @click.once="reward">领取奖励(只能点一次)</button>
4. 修饰符可以串联,顺序有意义:
vue
<a @click.stop.prevent="doThat">又阻止冒泡又阻止默认</a>
六、综合实战:防误触弹窗
把 .stop / .self / .prevent / .once 全用上:
vue
<template>
<button @click="show = true">打开弹窗</button>
<!-- .self:只有点遮罩本身才关(点内容区不算) -->
<div v-if="show" class="mask" @click.self="show = false">
<div class="dialog">
<h3>确认操作</h3>
<a href="https://example.com/pay" @click.prevent.once="pay">
首次点击支付(阻止跳转 + 只生效一次)
</a>
<button @click="show = false">关闭</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
function pay() {
console.log('调起支付逻辑,页面不跳转')
}
</script>
<style scoped>
.mask {
position: fixed; inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex; align-items: center; justify-content: center;
}
.dialog {
background: #fff; padding: 24px; border-radius: 8px;
}
</style>
三个细节品味:
.self比.stop更优雅:点弹窗内容冒泡到遮罩,但因为"目标不是遮罩自己",不触发关闭------弹窗标准写法.prevent.once串联:第一次点击走 pay 逻辑且不跳转,第二次起链接恢复默认行为(演示用,实际支付按钮该用 disabled)- v-if 控制弹窗出生销毁(015 篇知识复用)
【截图位置:点击遮罩关闭、点击内容区不关闭的运行效果】
七、踩坑记录
- @click="fn()" 与 @click="fn":带括号是"模板解析时求值后的调用时机控制",不带括号默认第一参数是事件对象;带括号还想拿事件对象必须显式传 $event(004 篇坑的完整版答案)
- .stop 和 .self 混淆:.stop 是"我不让往上冒",.self 是"不是点我就不理"------弹窗用 .self,按钮防冒泡用 .stop
- 修饰符顺序 :
@click.prevent.self会阻止所有点击 的默认行为(包括子元素);@click.self.prevent只阻止"点自己"时的默认行为------顺序不同语义不同 - Vue3 移除了 .native 修饰符:Vue2 给组件根元素绑原生事件要 .native,Vue3 交给 attrs 透传机制(033 篇细讲),现在直接写会警告
- passive 别乱加 :
@scroll.passive提升滚动性能,但加了就不能在里面 e.preventDefault() 了
八、今日小结
- $event 是原生事件对象的专用占位,传参与事件对象共存
- 多个同事件处理器用数组:
@[a, b] - 三大高频修饰符:.stop 防冒泡、.prevent 防默认、.once 一次性
- 弹窗遮罩标准姿势:
@click.self;修饰符可串联且顺序有语义
下篇预告
点击说完了,键盘怎么办?"按回车就搜索""按 Esc 关弹窗""Ctrl+S 保存"------下一篇 018 按键与鼠标修饰符,让键盘事件优雅到飞起。