讲解
模板里绑定事件用 @(v-on 的简写):<button @click="add"> 直接引用方法,<button @click="add(1)"> 内联调用传参。需要原生事件对象时,内联写法里用特殊变量 $event:@click="add(1, $event)"。推荐尽量用「引用方法」的写法——逻辑收在 script 里可测试,模板只留一行声明。
Vue 提供事件修饰符,把高频 DOM 操作变成声明式后缀:.prevent 阻止默认行为(表单提交不刷新页面)、.stop 阻止冒泡、.once 只触发一次、.self 只在事件目标是元素自身时触发。它们可以链式组合:@click.stop.prevent。按键修饰符让键盘事件更直观:@keyup.enter="submit"、@keydown.esc="close",组合键 @click.ctrl="multiSelect"。这些修饰符不改变事件本身,只是把「if (e.key !== 'Enter') return」这类样板代码收进了模板声明里。
表单之外,「事件从哪里来」决定了逻辑写在哪一层:原生 DOM 事件在模板里绑,组件自定义事件(下一章的 emits)在父组件里绑。Vue 3 中 @ 绑定的名字对原生事件是 DOM 事件名(click/input),对组件事件是 emits 声明的名字——同一个语法,两个世界,分清事件源是排查「为什么回调没触发」的第一步。
示例
用纯 JS 实现一个修饰符执行链,看 .stop 和 .once 各自拦下了什么(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 迷你事件系统:支持 stop / once / prevent 修饰符
function createHandler(fn, modifiers = {}) {
let called = 0;
return function handler(event) {
if (modifiers.once && called > 0) return '已被 once 拦截';
if (modifiers.stop) event.stopped = true;
if (modifiers.prevent) event.prevented = true;
called++;
return fn(event);
};
}
const log = [];
const save = createHandler(
(e) => log.push('保存:' + e.target) && '已执行',
{ prevent: true },
);
const onceVote = createHandler(() => '投票成功', { once: true });
const inner = createHandler(() => '内部响应', { stop: true });
// 表单事件:prevent 生效
const formEvent = { target: 'form' };
save(formEvent);
assert.strictEqual(formEvent.prevented, true);
assert.deepStrictEqual(log, ['保存:form']);
// once:第二次被拦截
assert.strictEqual(onceVote({}), '投票成功');
assert.strictEqual(onceVote({}), '已被 once 拦截');
// stop:冒泡被切断
const evt = {};
inner(evt);
assert.strictEqual(evt.stopped, true);
// 按键过滤逻辑:等价于 @keyup.enter
function onKeyup(e, cb) {
if (e.key === 'Enter') cb();
}
let submitted = 0;
onKeyup({ key: 'a' }, () => submitted++);
onKeyup({ key: 'Enter' }, () => submitted++);
assert.strictEqual(submitted, 1);
console.log('prevent 生效:', formEvent.prevented);
console.log('once 第二次:', onceVote({}));
console.log('只有 Enter 提交,提交次数:', submitted);
模板中的常用组合:
<script setup>
import { ref } from 'vue';
const text = ref('');
const history = ref([]);
function submit() {
if (!text.value.trim()) return;
history.value.push(text.value);
text.value = '';
}
</script>
<template>
<!-- 回车提交 + 阻止表单默认刷新 -->
<form @submit.prevent="submit">
<input v-model="text" @keyup.enter="submit" placeholder="回车添加" />
</form>
<ul>
<li v-for="(h, i) in history" :key="i">{{ h }}</li>
</ul>
</template>
常见坑
- 绑定写成调用:
@click="add()"变成每次渲染都执行一次回调(实际绑定的是返回值),引用写法不要加括号。 - 需要事件对象却忘了 $event:内联写法里事件不会自动传入,要显式写
$event。 - .prevent 用错位置:它是模板修饰符,写在方法名后面;在 JS 里就是 e.preventDefault(),别混用。
- 键盘事件绑在 div 上:div 默认不可聚焦收不到键盘事件,要么用 input,要么加 tabindex,要么绑在 window(注意卸载清理)。
- 把 @click 当组件原生点击:自定义组件上的 @click 是组件 emits 的事件,不是 DOM 点击;想监听组件根元素点击,组件内部要 emits('click') 透传。
小结
@ 绑事件、$event 拿事件对象、修饰符消样板;分清「DOM 事件」和「组件事件」两个来源。下一章讲表单的核心:v-model 双向绑定。