Vue.js: 表单与 v-model

最后更新:2026-10-20

表单是 Web 应用的核心交互——登录、注册、搜索、设置都离不开表单处理。Vue 的 v-model 是处理表单的"语法糖",让双向绑定代码极简(1 行 vs 5 行)。

Vue 3.4+ 的 defineModel 进一步简化 v-model 实现。复杂表单还需要校验、序列化、文件上传等高级功能。本课覆盖 5 大表单场景。

相关指令:v-model

1. 你将学到


2. 一个登录表单的"5 处 boilerplate"噩梦

(1) 痛点:5 个表单字段,10 行 v-model 代码

Alice 的登录表单有 5 个字段,每个都有 v-model 绑定:

5 字段 = 10 行样板代码。10 字段就是 20 行。表单越复杂越糟。

(2) Vue v-model 进阶解法:reactive 对象 + defineModel

5 字段 = 1 个对象。reactive 让代码量减少 50%。

(3) 收益

v-model 优化后:


3. v-model 原理

(1) v-model 本质

VUE
<!-- v-model 是 props + emit 的语法糖 -->
<input v-model="searchQuery">

<!-- 等同于 -->
<input 
  :value="searchQuery" 
  @input="searchQuery = $event.target.value"
>

(2) 组件中的 v-model

VUE
<!-- 父组件使用 v-model -->
<CustomInput v-model="searchQuery" />

<!-- 等同于 -->
<CustomInput 
  :modelValue="searchQuery" 
  @update:modelValue="searchQuery = $event"
/>

(3) defineModel(Vue 3.4+ 简化)


4. 5 种自定义 v-model

(1) 基本 input

(2) textarea

(3) select

(4) checkbox(多选)

(5) 自定义修饰符(trim)

VUE
<!-- 父组件:v-model.trim -->
<CustomInput v-model.trim="username" />

<!-- 子组件:处理 trim 修饰符 -->
<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])

function onInput(e) {
  let value = e.target.value
  if (props.modelModifiers.trim) {
    value = value.trim()
  }
  emit('update:modelValue', value)
}
</script>

▶ 示例:5 种自定义 v-model + 多 v-model 速查

▶ 试一试

输出:

TEXT
自定义输入框显示父组件传入的 search 值;输入时触发 update:modelValue,父组件的 search 同步更新。
使用 v-model:username / v-model:password 时,两个字段各自独立双向绑定,修改一个不会影响另一个。

5. 表单校验

(1) 自定义校验

JS
// utils/validators.js
export function isEmail(value) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}

export function isPhone(value) {
  return /^1[3-9]\d{9}$/.test(value)
}

export function minLength(value, min) {
  return value.length >= min
}
VUE
<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.email" @blur="validateField('email')">
    <p v-if="errors.email" class="error">{{ errors.email }}</p>
    
    <input v-model="form.password" type="password">
    <p v-if="errors.password">{{ errors.password }}</p>
    
    <button :disabled="!isValid">Submit</button>
  </form>
</template>

<script setup>
const { reactive, ref } = Vue
import { isEmail, minLength } from '@/utils/validators'

const form = reactive({
  email: '',
  password: ''
})

const errors = reactive({})
const isValid = ref(false)

function validateField(field) {
  if (field === 'email') {
    if (!form.email) {
      errors.email = 'Email is required'
    } else if (!isEmail(form.email)) {
      errors.email = 'Invalid email format'
    } else {
      delete errors.email
    }
  }
  
  if (field === 'password') {
    if (!form.password) {
      errors.password = 'Password is required'
    } else if (!minLength(form.password, 8)) {
      errors.password = 'Password must be 8+ characters'
    } else {
      delete errors.password
    }
  }
  
  isValid.value = Object.keys(errors).length === 0
}

function handleSubmit() {
  Object.keys(form).forEach(validateField)
  if (isValid.value) {
    console.log('Submit:', form)
  }
}
</script>

(2) VeeValidate 库(推荐)

BASH
npm install vee-validate @vee-validate/rules
VUE
<template>
  <Form @submit="handleSubmit" :validation-schema="schema">
    <Field name="email" type="email" v-model="form.email" />
    <ErrorMessage name="email" />
    
    <Field name="password" type="password" v-model="form.password" />
    <ErrorMessage name="password" />
    
    <button>Submit</button>
  </Form>
</template>

<script setup>
import { Form, Field, ErrorMessage } from 'vee-validate'
import * as yup from 'yup'

const schema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().min(8).required()
})

const form = reactive({ email: '', password: '' })

function handleSubmit(values) {
  console.log('Valid:', values)
}
</script>

6. 复杂表单设计

(1) 嵌套表单

JS
const form = reactive({
  user: {
    name: '',
    email: '',
    age: 0
  },
  address: {
    city: '',
    country: ''
  }
})

(2) 动态字段(v-for)

JS
const form = reactive({
  items: [
    { name: '', quantity: 1, price: 0 }
  ]
})

function addItem() {
  form.items.push({ name: '', quantity: 1, price: 0 })
}

function removeItem(index) {
  form.items.splice(index, 1)
}

(3) 5 大表单修饰符

VUE
<!-- 1. .lazy:change 事件触发(不是 input) -->
<input v-model.lazy="email">

<!-- 2. .number:自动转数字 -->
<input v-model.number="age" type="number">

<!-- 3. .trim:自动去空格 -->
<input v-model.trim="username">

<!-- 4. 自定义修饰符(CustomInput) -->
<CustomInput v-model.trim="text" />

<!-- 5. .debounce(自定义修饰符):DebouncedInput 组件内部通过 modelModifiers 实现 -->
<DebouncedInput v-model.debounce="searchText" />

▶ 示例:5 大表单修饰符

VUE
<!-- 1. .lazy:change 事件(失焦后)-->
<input v-model.lazy="email">

<!-- 2. .number:自动转数字 -->
<input v-model.number="age" type="number">

<!-- 3. .trim:自动 trim -->
<input v-model.trim="username">

<!-- 4. 多修饰符组合 -->
<input v-model.lazy.trim="email">

<!-- 5. 自定义修饰符(子组件) -->
<CustomInput v-model.trim="text" />
▶ 试一试

输出:

TEXT
.lazy 输入时不同步,失焦或回车后才更新绑定的 email。
.number 把输入的年龄自动转成数字类型;.trim 自动去掉用户名首尾空格。
组合 .lazy.trim 在失焦时一次性更新并去除空格;子组件上的 v-model.trim 会把修饰符传入。

7. 文件上传

(1) 单文件上传

(2) 多文件 + 拖拽


8. 综合演练:注册表单校验实战

▶ 示例:5 种 v-model 基础 + 校验

▶ 展开运行示例(63 行)
▶ 试一试

输出:

TEXT
页面显示「注册表单(5 字段 + 校验)」和 5 个输入框,字段下方错误提示为空,「注册」按钮置灰。
用户名留空失焦提示 Required;邮箱不含 @ 提示 Invalid email;年龄小于 18 提示 Age must be 18-120;密码不足 8 位、两次密码不一致也分别给出提示。
全部字段合法后出现绿色「✅ 表单通过校验!可提交」,「注册」按钮变为可点,点击弹出包含表单 JSON 的 alert。

(1) 📊 5 个常见错误速查

错误 现象 解决
v-model.number 仍为字符串 加法运算异常 type="number" + .number
v-model 不更新 ref 改为 reactive 后失效 改 .value 或 toRefs
文件上传 formData 失败 跨域或 method 错 fetch + FormData
校验时机不对 失去焦点才显示 用 @blur
大量字段 boilerplate 代码冗长 用 reactive 对象

(2) 📊 5 大性能对比

模式 简洁度 性能 适用
多个 ref ❌ 重复 ⭐⭐⭐⭐ 1-2 字段
reactive 对象 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 5+ 字段
FormData 手动 ❌ ⭐⭐⭐ 文件上传
VeeValidate ⭐⭐⭐⭐ ⭐⭐⭐ 复杂校验
defineModel ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ Vue 3.4+ 组件

相关指令:v-model

❓ 常见问题

Q v-model 在自定义组件中怎么用?
A 组件内 defineProps(['modelValue']) + defineEmits(['update:modelValue'])。父组件用 v-model。Vue 3.4+ 用 defineModel 1 行替代 10 行。
Q v-model.number 不生效?
A 必须配合 type="number"。或者手动 parseFloat()。
Q 表单校验用自定义还是 VeeValidate?
A 简单表单(< 5 字段)自定义;复杂表单(> 5 字段 + 嵌套 + 异步校验)用 VeeValidate。
Q 怎么上传文件并显示预览?
A 用 FileReader.readAsDataURL() 读为 base64 字符串,赋值给 <img src="...">。上传时用 FormData。
Q v-model 修饰符 .lazy 和 .debounce 区别?
A .lazy 在 change 事件触发(失焦后),节省实时校验开销。.debounce 是自定义修饰符,需要在子组件实现,300ms 内最后一次触发。
Q 表单数据怎么序列化为 JSON 提交?
A 直接用 JSON.stringify(form)(无需 FormData,除非有文件)。后端接收 JSON 即可。
Q defineModel 替代 v-model 完整写法吗?
A 是的。defineModel('modelValue') 等同于 props.modelValue + emit('update:modelValue') 的 10 行代码。完全向后兼容。

📖 小节


📝 作业

  1. 基础题(难度⭐) 实现一个简单的登录表单:

    • 5 个字段(username / password / email / phone / captcha)
    • v-model + reactive
    • 提交时打印整个 form 对象
  2. 进阶题(难度⭐⭐) 实现表单校验:

    • 5 个字段 + 自定义校验(email 格式、phone 格式、password 长度)
    • @blur 触发校验
    • 错误信息显示
    • 提交按钮根据 isValid 禁用/启用
  3. 挑战题(难度⭐⭐⭐) 实现完整的"用户注册"表单系统:

    1. 10 个字段(含嵌套 address 对象 + 动态 tags 数组)
    2. VeeValidate + Yup schema
    3. 文件上传(头像)
    4. 5 种修饰符全覆盖
    5. defineModel 用于子组件
    6. 5 种错误状态
    7. TypeScript 强类型
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏