讲解

模板里绑定事件用 @(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 双向绑定。