☰
事件处理与表单输入绑定
2026/11/4 5:54:45 网站建设 项目流程

事件处理与表单输入绑定

数据驱动视图,事件连接用户。会处理点击、输入、键盘和提交,才能把静态页面变成真正可用的应用。

在上一篇文章中,我们使用条件渲染和列表渲染搭建了动态页面。本文继续完善交互能力,系统学习 Vue 事件处理与表单输入绑定,并通过一个注册表单把知识串联起来。

写作约定:本系列以 Vue 3<script setup>为主要写法。示例默认使用 Vue 3.2+,重点关注实际开发中的事件参数、修饰符选择、表单状态和常见陷阱。


一、事件处理:让用户操作驱动状态

Vue 使用v-on监听 DOM 事件,缩写为@。事件发生后,我们通常会修改响应式数据,视图再自动更新。

1.1 基本用法

<template> <button v-on:click="count++">完整写法:{{ count }}</button> <button @click="increment">简写:{{ count }}</button> </template> <script setup> import { ref } from 'vue' const count = ref(0) const increment = () => { count.value++ } </script>

count++属于内联事件处理器,适合简单逻辑;increment属于方法事件处理器,更适合复用和测试。

1.2 传递自定义参数

<template> <button @click="add(2)">每次加 2</button> <button @click="add(5)">每次加 5</button> <p>当前计数:{{ count }}</p> </template> <script setup> import { ref } from 'vue' const count = ref(0) const add = (step) => { count.value += step } </script>

方法名后面直接跟括号即可传参。需要同时传自定义参数和原生事件对象时,使用$event:

<template> <button @click="record('save', $event)">保存</button> </template> <script setup> const record = (action, event) => { console.log('操作类型:', action) console.log('事件对象:', event) console.log('触发元素:', event.currentTarget) } </script>

$event是 Vue 提供的原生 DOM 事件对象。只在需要事件信息时传递,不必在每个方法中机械接收。

1.3 内联访问事件对象

<template> <input @input="keyword = $event.target.value" placeholder="输入关键词"> <p>当前关键词:{{ keyword }}</p> </template> <script setup> import { ref } from 'vue' const keyword = ref('') </script>

在简单场景下,这种写法能直接表达意图。但逻辑变复杂后,应抽成方法,避免模板充斥 JavaScript 表达式。


二、事件修饰符:少写重复的事件代码

Vue 提供事件修饰符处理常见需求,如阻止冒泡、阻止默认行为和只触发一次。

2.1 stop 与 prevent

<template> <div @click="handleOuter"> <button @click.stop="handleInner">阻止点击冒泡</button> </div> <form @submit.prevent="handleSubmit"> <button type="submit">阻止表单默认提交</button> </form> </template> <script setup> const handleOuter = () => console.log('外层点击') const handleInner = () => console.log('内层点击') const handleSubmit = () => console.log('处理提交') </script>
  • .stop:调用event.stopPropagation(),阻止事件继续冒泡。
  • .prevent:调用event.preventDefault(),阻止浏览器默认行为。

不要看到表单就一律使用.prevent。如果需要浏览器原生校验、原生跳转或默认下载行为,应保留默认动作。

2.2 capture、self 与 once

<template> <div @click.capture="handleCapture"> <button @click.self="handleSelf">只在元素本身触发</button> <button @click.once="handleOnce">只执行一次</button> </div> </template> <script setup> const handleCapture = () => console.log('捕获阶段处理') const handleSelf = () => console.log('只处理自身点击') const handleOnce = () => console.log('只执行一次') </script>
  • .capture:在捕获阶段监听,而不是默认的冒泡阶段。
  • .self:只有event.target是当前元素本身时才触发,不包括子元素。
  • .once:事件处理器最多执行一次。

2.3 passive:改善滚动性能

<template> <div @scroll.passive="handleScroll" class="scroll-area"> <p v-for="n in 50" :key="n">第 {{ n }} 行内容</p> </div> </template> <script setup> const handleScroll = () => { // 只读取滚动位置,不调用 preventDefault } </script> <style scoped> .scroll-area { height: 180px; overflow-y: auto; } </style>

.passive表示监听器不会调用preventDefault,浏览器可以并行处理默认滚动行为。它适合scroll、touchstart、touchmove等事件,但不适合需要阻止默认行为的场景。

2.4 修饰符可以组合

<template> <form @submit.stop.prevent="handleSubmit"> <button type="submit">同时阻止冒泡和默认提交</button> </form> </template>

修饰符顺序会影响生成的代码,但常见组合通常不需要特别记忆。更重要的是明确每个修饰符的业务目的,避免无意义叠加。


三、按键与鼠标修饰符

3.1 常用按键修饰符

<template> <input @keyup.enter="submit" placeholder="回车提交"> <input @keyup.esc="cancel" placeholder="Esc 取消"> <input @keyup.delete="remove" placeholder="Delete 删除"> <input @keyup.space="toggle" placeholder="空格切换"> <input @keyup.up="moveUp" placeholder="上方向键"> <input @keyup.down="moveDown" placeholder="下方向键"> <input @keyup.left="moveLeft" placeholder="左方向键"> <input @keyup.right="moveRight" placeholder="右方向键"> </template> <script setup> const submit = () => console.log('提交') const cancel = () => console.log('取消') const remove = () => console.log('删除') const toggle = () => console.log('切换') const moveUp = () => console.log('向上') const moveDown = () => console.log('向下') const moveLeft = () => console.log('向左') const moveRight = () => console.log('向右') </script>

tab也可以使用,但它会改变焦点位置,应结合可访问性谨慎处理。

3.2 系统修饰键与精确匹配

<template> <button @click.ctrl="handleCtrl">Ctrl + 点击</button> <button @click.alt="handleAlt">Alt + 点击</button> <button @click.shift="handleShift">Shift + 点击</button> <button @click.meta="handleMeta">Command / Windows + 点击</button> <button @click.ctrl.exact="handleCtrlOnly">只按 Ctrl,不能有其他修饰键</button> <button @click.exact="handlePlainClick">不能按任何系统修饰键</button> </template> <script setup> const handleCtrl = () => console.log('Ctrl 点击') const handleAlt = () => console.log('Alt 点击') const handleShift = () => console.log('Shift 点击') const handleMeta = () => console.log('Meta 点击') const handleCtrlOnly = () => console.log('仅 Ctrl 点击') const handlePlainClick = () => console.log('普通点击') </script>

.exact用于精确控制组合键,避免用户同时按下其他修饰键时误触发。

3.3 鼠标按钮修饰符

<template> <button @click.left="handleLeft">左键</button> <button @click.right="handleRight">右键</button> <button @click.middle="handleMiddle">中键</button> </template>

需要完全自定义键盘判断时,也可以直接读取event.key:

<template> <input @keydown="handleKeydown" placeholder="输入内容"> </template> <script setup> const handleKeydown = (event) => { if (event.key === 'Enter' && !event.isComposing) { console.log('确认输入') } } </script>

event.isComposing可用于避免中文输入法组词过程中按回车选词时误提交。


四、v-model:表单与状态的双向绑定

v-model是表单输入绑定的核心。它本质上同时完成属性绑定和事件监听,让输入内容自动同步到响应式数据。

4.1 文本与多行文本

<template> <input v-model="title" placeholder="文章标题"> <textarea v-model="summary" placeholder="文章摘要"></textarea> <p>标题:{{ title }}</p> <p>摘要:{{ summary }}</p> </template> <script setup> import { ref } from 'vue' const title = ref('') const summary = ref('') </script>

在文本输入框中,v-model近似等价于:

<template> <input :value="title" @input="title = $event.target.value"> </template>

理解这一点后,就能知道什么时候可以使用v-model,什么时候需要手动拆分:value和@input。

4.2 单个复选框与多个复选框

单个复选框通常绑定布尔值,多个复选框通常绑定数组:

<template> <label> <input type="checkbox" v-model="accept"> 接受用户协议 </label> <p>是否接受:{{ accept }}</p> <fieldset> <legend>选择技能</legend> <label><input type="checkbox" value="Vue" v-model="skills">Vue</label> <label><input type="checkbox" value="React" v-model="skills">React</label> <label><input type="checkbox" value="TypeScript" v-model="skills">TypeScript</label> </fieldset> <p>已选择:{{ skills }}</p> </template> <script setup> import { ref } from 'vue' const accept = ref(false) const skills = ref([]) </script>

多个复选框的value应保持稳定且能表达业务含义,初始化时使用数组。

4.3 单选框

<template> <label><input type="radio" value="user" v-model="role">普通用户</label> <label><input type="radio" value="editor" v-model="role">编辑</label> <label><input type="radio" value="admin" v-model="role">管理员</label> <p>当前角色:{{ role }}</p> </template> <script setup> import { ref } from 'vue' const role = ref('user') </script>

单选框共享同一个v-model,选中项的value会同步到该变量。

4.4 下拉选择框

<template> <select v-model="city"> <option value="">请选择城市</option> <option value="jinan">济南</option> <option value="qingdao">青岛</option> <option value="yantai">烟台</option> </select> <select v-model="languages" multiple size="3"> <option value="javascript">JavaScript</option> <option value="typescript">TypeScript</option> <option value="java">Java</option> </select> <p>城市:{{ city }}</p> <p>语言:{{ languages }}</p> </template> <script setup> import { ref } from 'vue' const city = ref('') const languages = ref([]) </script>

多选select同样绑定数组。没有默认选中项时,单选下拉框可绑定空字符串。


五、v-model 修饰符

5.1 lazy:从 input 改为 change 同步

<template> <input v-model="keyword" placeholder="默认每次输入都同步"> <input v-model.lazy="keyword" placeholder="失焦或回车时同步"> </template> <script setup> import { ref } from 'vue' const keyword = ref('') </script>

.lazy适合不希望每次按键都触发复杂计算的输入框。是否需要它取决于交互和性能,而不是简单地认为它“更省性能”。

5.2 number:尽量转换为数字

<template> <input v-model.number="age" type="number" placeholder="年龄"> <input v-model.number="price" type="number" placeholder="价格"> <p>年龄类型:{{ typeof age }}</p> <p>价格类型:{{ typeof price }}</p> </template> <script setup> import { ref } from 'vue' const age = ref(18) const price = ref(99) </script>

.number会尝试把输入转换为数字;无法解析为有效数字时保留原始字符串,因此需要数值计算或校验时仍应检查。

5.3 trim:自动去除首尾空格

<template> <input v-model.trim="username" placeholder="用户名"> <p>长度:{{ username.length }}</p> </template> <script setup> import { ref } from 'vue' const username = ref('') </script>

.trim只去除首尾空格,不会删除字符串中间的空格。修饰符也可以组合:

<template> <input v-model.lazy.number.trim="amount" type="number" placeholder="金额"> </template>

六、表单处理的推荐结构

实际表单通常使用reactive管理一组字段,使用计算属性处理派生状态和校验结果。

<script setup> import { computed, reactive, ref } from 'vue' const form = reactive({ username: '', email: '', role: 'user', skills: [], agree: false }) const submitted = ref(false) const canSubmit = computed(() => { return form.username.trim() && form.email.includes('@') && form.skills.length > 0 && form.agree }) const handleSubmit = () => { if (!canSubmit.value) return submitted.value = true } </script>

推荐区分三类数据:

类型示例推荐管理方式
表单原始值用户名、邮箱、角色、是否同意reactive(form)
派生状态是否可提交、密码强度、过滤结果computed
页面状态是否提交中、成功或失败提示、是否展示密码ref

不要在模板中重复编写复杂校验表达式,也不要把“提交中”这类页面状态混进表单原始值。


七、综合实战:用户注册表单

下面实现一个包含文本、邮箱、密码、单选、多选、下拉框和协议确认的注册表单。它包含提交拦截、字段校验、错误提示和重置功能。

<template> <form @submit.prevent="handleSubmit" class="register-form"> <h2>用户注册</h2> <div class="field"> <label for="username">用户名</label> <input id="username" v-model.trim="form.username" @blur="touch('username')" placeholder="至少 3 个字符"> <p v-if="errors.username" class="error">{{ errors.username }}</p> </div> <div class="field"> <label for="email">邮箱</label> <input id="email" v-model.trim="form.email" @blur="touch('email')" type="email" placeholder="name@example.com"> <p v-if="errors.email" class="error">{{ errors.email }}</p> </div> <div class="field"> <label for="password">密码</label> <input id="password" v-model="form.password" @blur="touch('password')" type="password" placeholder="至少 8 个字符"> <p v-if="errors.password" class="error">{{ errors.password }}</p> </div> <div class="field"> <label for="confirmPassword">确认密码</label> <input id="confirmPassword" v-model="form.confirmPassword" @blur="touch('confirmPassword')" type="password" placeholder="再次输入密码"> <p v-if="errors.confirmPassword" class="error">{{ errors.confirmPassword }}</p> </div> <div class="field"> <label for="role">角色</label> <select id="role" v-model="form.role"> <option value="user">普通用户</option> <option value="editor">内容编辑</option> <option value="reviewer">审核人员</option> </select> </div> <fieldset> <legend>关注技术</legend> <label><input type="checkbox" value="vue" v-model="form.skills">Vue</label> <label><input type="checkbox" value="typescript" v-model="form.skills">TypeScript</label> <label><input type="checkbox" value="java" v-model="form.skills">Java</label> <p v-if="errors.skills" class="error">{{ errors.skills }}</p> </fieldset> <div class="field"> <label for="bio">个人简介</label> <textarea id="bio" v-model.trim="form.bio" placeholder="可选,最多 100 字"></textarea> </div> <label> <input type="checkbox" v-model="form.agree"> 我已阅读并同意用户协议 <span v-if="errors.agree" class="error">{{ errors.agree }}</span> </label> <div class="actions"> <button type="button" @click="resetForm">重置</button> <button type="submit" :disabled="submitting">{{ submitting ? '提交中...' : '注册' }}</button> </div> <p v-if="successMessage" class="success">{{ successMessage }}</p> </form> </template> <script setup> import { computed, reactive, ref } from 'vue' const createInitialForm = () => ({ username: '', email: '', password: '', confirmPassword: '', role: 'user', skills: [], bio: '', agree: false }) const form = reactive(createInitialForm()) const touched = reactive({}) const submitting = ref(false) const successMessage = ref('') const touch = (field) => { touched[field] = true } const errors = computed(() => { const result = {} if (touched.username && form.username.length < 3) { result.username = '用户名至少 3 个字符' } if (touched.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) { result.email = '请输入有效邮箱地址' } if (touched.password && form.password.length < 8) { result.password = '密码至少 8 个字符' } if (touched.confirmPassword && form.confirmPassword !== form.password) { result.confirmPassword = '两次输入的密码不一致' } if (touched.skills && form.skills.length === 0) { result.skills = '至少选择一项技术' } if (touched.agree && !form.agree) { result.agree = '请先同意用户协议' } return result }) const isValid = computed(() => { return form.username.length >= 3 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email) && form.password.length >= 8 && form.confirmPassword === form.password && form.skills.length > 0 && form.agree }) const handleSubmit = async () => { const requiredFields = ['username', 'email', 'password', 'confirmPassword', 'skills', 'agree'] requiredFields.forEach(touch) successMessage.value = '' if (!isValid.value || submitting.value) return submitting.value = true await new Promise(resolve => setTimeout(resolve, 500)) successMessage.value = `注册成功,欢迎 ${form.username}!` submitting.value = false } const resetForm = () => { Object.assign(form, createInitialForm()) Object.keys(touched).forEach(key => delete touched[key]) successMessage.value = '' } </script> <style scoped> .register-form { max-width: 520px; margin: 24px auto; display: grid; gap: 16px; } .field { display: grid; gap: 6px; } input, select, textarea { padding: 8px 10px; border: 1px solid #d1d5db; border-radius: 6px; } fieldset { display: flex; gap: 12px; border: 0; padding: 0; } .actions { display: flex; gap: 12px; } .error { margin: 0; color: #dc2626; font-size: 13px; } .success { color: #059669; } </style>

这个示例有几个值得注意的点:

  1. touch()只负责记录字段是否失焦,避免首次打开页面就显示全部错误。
  2. errors负责展示,isValid负责最终提交判断,两者职责不同。
  3. 提交前统一标记相关字段,确保未失焦字段也能显示错误。
  4. resetForm()同时重置表单值、失焦状态和提示信息,避免旧状态残留。

八、常见问题自查

问题推荐处理
点击子元素也触发父级事件使用.stop或.self,按实际结构选择
表单点击后页面刷新使用@submit.prevent并自行处理提交
回车选词时误提交检查event.isComposing
多选框无法绑定初始值使用数组,选项提供稳定value
数字计算出现字符串拼接使用.number并检查解析结果
页面打开就显示全部错误失焦后再提示,提交时统一校验
重置后仍有错误提示同时重置表单值、失焦状态和页面提示
滚动事件卡顿不需要阻止默认行为时考虑.passive

九、总结

事件处理与表单绑定是 Vue 交互开发的基础。本文最重要的几点是:

  1. @事件名是v-on的简写,自定义参数与$event可以一起使用。
  2. 事件修饰符应服务于明确业务目的,常见选择包括.stop、.prevent、.once和.passive。
  3. 处理回车时注意中文输入法组词状态,必要时检查event.isComposing。
  4. v-model同时完成属性绑定和事件监听,复选框与多选下拉框通常绑定数组。
  5. .lazy、.number、.trim分别控制同步时机、数字转换和首尾空格。
  6. 复杂表单应拆分原始值、派生状态和页面状态,校验逻辑不要堆在模板中。

下一站预告

下一篇《Class与Style绑定》将学习如何根据状态动态管理类名、样式和组件外观,让表单和列表不仅可用,也拥有清晰的状态反馈。


作者:洋洋技术笔记
发布日期:2026-11-03
系列:Vue.js从入门到精通 - 第6篇


原文链接:洋洋的技术笔记

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询