侧边栏壁纸
博主头像
洋洋的技术笔记

行动起来,活在当下

  • 累计撰写 12 篇文章
  • 累计创建 2 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录
VUE

06-事件处理与表单输入绑定

洋洋的技术笔记
2026-11-03 / 0 评论 / 0 点赞 / 9 阅读 / 0 字

事件处理与表单输入绑定

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

在上一篇文章中,我们使用条件渲染和列表渲染搭建了动态页面。本文继续完善交互能力,系统学习 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篇


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

0

评论区