Vue 表单与 v-model
表单是前端最繁琐的部分:文本框、复选框、单选、下拉、文件上传……每种控件的取值方式都不一样。Vue 用一个 v-model 指令把所有表单控件统一抽象成"双向绑定",极大简化了表单开发。本章覆盖所有常见表单场景。
1. v-model 的本质
先理解 v-model 到底在做什么。它本质是两个绑定的语法糖:
:value="data":把数据绑定到控件的 value 属性(数据 → 视图)。@input="data = $event.target.value":输入时把新值同步回数据(视图 → 数据)。
对每种控件(input/checkbox/radio/select),Vue 内部会自动用对应的属性和事件,你只需要写 v-model,剩下的交给 Vue。
2. 文本框与 textarea
最基础的场景:<input> 和 <textarea> 都直接用 v-model。
<template>
<!-- 文本框:输入即同步 -->
<input v-model="username" placeholder="用户名" />
<p>你好,{{ username }}</p>
<!-- 多行文本:同样用 v-model,注意不要写成 {{ }} -->
<textarea v-model="content" placeholder="说点什么"></textarea>
<p>你输入了:{{ content }}</p>
<!-- 密码框 -->
<input type="password" v-model="password" />
</template>
<script setup>
import { ref } from "vue";
const username = ref("");
const content = ref("");
const password = ref("");
</script>注意:textarea 里不要写插值 {{ content }}——它只显示初始值,后续不会更新。永远用 v-model。
3. 复选框(checkbox)
复选框分两种用法:单个绑布尔值、多个绑数组。
<template>
<!-- 单个复选框:绑定布尔值 -->
<input type="checkbox" v-model="agree" /> 同意条款
<p>{{ agree ? "已同意" : "未同意" }}</p>
<!-- 多个复选框:绑定数组,每个 input 写不同的 value -->
<p>选你喜欢的:
<label><input type="checkbox" v-model="hobbies" value="read" /> 阅读</label>
<label><input type="checkbox" v-model="hobbies" value="music" /> 音乐</label>
<label><input type="checkbox" v-model="hobbies" value="sport" /> 运动</label>
</p>
<p>已选:{{ hobbies.join(", ") }}</p>
</template>
<script setup>
import { ref } from "vue";
const agree = ref(false);
const hobbies = ref([]); // 注意初始值是数组
</script>多个复选框的关键点:多个 checkbox 用同一个 v-model,每个 input 写不同的 value,Vue 会自动把选中的 value 收集成数组。初始值必须是数组([])。
4. 单选框(radio)
单选框用法和"多复选框"几乎一样——同一个 v-model,不同的 value。Vue 自动让它们互斥。
<template>
<!-- 单选框:绑定值,每个 input 写不同的 value -->
<p>性别:
<label><input type="radio" v-model="gender" value="M" /> 男</label>
<label><input type="radio" v-model="gender" value="F" /> 女</label>
</p>
<p>你选了:{{ gender }}</p>
<!-- 单选按钮组:同一个 v-model 自动互斥 -->
<p>评分:
<label v-for="n in 5" :key="n">
<input type="radio" v-model="rating" :value="n" /> {{ n }}
</label>
</p>
</template>
<script setup>
import { ref } from "vue";
const gender = ref("M");
const rating = ref(5);
</script>用 v-for 渲染单选项是常见模式,记得给 :value 和 :key。
5. 下拉选择(select)
select 的 v-model 写在 <select> 上(不是 option),选项通过 value 区分:
<template>
<!-- 单选下拉:绑定基本类型 -->
<select v-model="city">
<option value="">请选择</option>
<option value="bj">北京</option>
<option value="sh">上海</option>
<option value="gz">广州</option>
</select>
<p>你选了:{{ city }}</p>
<!-- 多选下拉:加 multiple,绑定数组 -->
<select v-model="tags" multiple>
<option value="vue">Vue</option>
<option value="react">React</option>
<option value="svelte">Svelte</option>
</select>
<p>标签:{{ tags.join(", ") }}</p>
<!-- v-for 动态渲染选项 -->
<select v-model="userId">
<option v-for="u in users" :key="u.id" :value="u.id">
{{ u.name }}
</option>
</select>
</template>
<script setup>
import { ref } from "vue";
const city = ref("");
const tags = ref([]);
const userId = ref(1);
const users = ref([
{ id: 1, name: "小明" },
{ id: 2, name: "小红" },
]);
</script>几个注意点:
- 单选:绑定基本类型(string/number)。
- 多选:加
multiple属性,绑定数组(按住 Ctrl/Cmd 点击)。 - 动态选项:用 v-for 渲染
<option>,记得给:value。 - 第一项最好有一个"请选择"占位选项,value 为空字符串。
6. v-model 的三个修饰符
修饰符改变 v-model 的同步行为:
<template>
<!-- 默认:每次 input 事件都同步(输入即变) -->
<input v-model="text1" />
<!-- .lazy:改为 change 事件触发(失焦或回车时才同步) -->
<input v-model.lazy="text2" />
<!-- .number:自动转数字(否则是字符串) -->
<input v-model.number="age" type="number" />
<p>age 类型:{{ typeof age }}</p>
<!-- .trim:自动去除首尾空格 -->
<input v-model.trim="name" />
<!-- 链式 -->
<input v-model.lazy.trim="name" />
</template>
<script setup>
import { ref } from "vue";
const text1 = ref("");
const text2 = ref("");
const age = ref(0);
const name = ref("");
</script>.lazy:默认每次 input 事件都同步(输入即变);加 lazy 后改为 change 事件触发(失焦或回车才同步)。适合不需要实时验证的字段,减少重渲染。.number:自动把输入转成数字类型。否则即使type="number",v-model 拿到的还是字符串("18"而非18),提交后端时容易出问题。.trim:自动去掉首尾空格。用户不小心输入空格时不污染数据。
修饰符可以链式:v-model.lazy.trim.number,三个都生效。
7. 表单的常见痛点与解法
- 密码强度实时计算:用 watch 监听 password,在回调里算强度。或直接 computed 派生。
- 提交前校验:自定义一个 validate 函数,遍历所有字段返回错误信息。复杂场景用 VeeValidate 库。
- 表单重置:把所有 ref 重置为初始值。复杂表单建议用 reactive 集中管理,一次性 Object.assign 重置。
- 未保存提示:用 watch 监听表单数据变化,设 dirty 标志;路由跳转前用 onBeforeRouteLeave 拦截。
8. 自定义组件的 v-model
Vue 3 允许把 v-model 用在你自己写的组件上——这是封装表单控件的核心机制。比如自己写一个带样式的输入框:
<!-- CustomInput.vue -->
<template>
<!-- 写法一:基于 modelValue prop + update:modelValue 事件 -->
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(["modelValue"]);
defineEmits(["update:modelValue"]);
</script>
<!-- 父组件 -->
<template>
<!-- v-model 用在自定义组件上 -->
<CustomInput v-model="msg" />
<!-- 等价于: -->
<CustomInput :model-value="msg" @update:model-value="msg = $event" />
</template>
<script setup>
import { ref } from "vue";
import CustomInput from "./CustomInput.vue";
const msg = ref("");
</script>原理:子组件接收 modelValue 作为 prop,触发 update:modelValue 事件通知父组件。父组件的 v-model 自动展开成这两个绑定。
Vue 3 还支持多个 v-model(给每个加参数):<UserForm v-model:name="name" v-model:age="age" />。子组件分别声明 name 和 age 即可。这让组件的复用性大大提升。
9. 实战:登录表单
把上面所有知识组合起来,写一个最经典的登录表单:
<template>
<form @submit.prevent="onSubmit">
<div>
<label>账号</label>
<input v-model.trim="form.account" placeholder="手机号" />
</div>
<div>
<label>密码</label>
<input type="password" v-model="form.password" />
</div>
<div>
<label><input type="checkbox" v-model="form.remember" /> 记住我</label>
</div>
<button type="submit">登录</button>
<p v-if="error" style="color: red">{{ error }}</p>
</form>
</template>
<script setup>
import { reactive, ref } from "vue";
const form = reactive({
account: "",
password: "",
remember: false,
});
const error = ref("");
function onSubmit() {
if (!form.account) return (error.value = "请输入账号");
if (form.password.length < 6) return (error.value = "密码至少 6 位");
error.value = "";
alert("登录成功:" + form.account);
}
</script>注意几个细节:
- 用
reactive把表单字段集中管理,提交时整体发送。 @submit.prevent阻止默认提交(否则页面刷新)。- 错误信息用 ref,验证失败时赋值,模板里 v-if 显示。
小结
这一章你掌握了 Vue 表单开发的所有基础:v-model 在不同控件上的用法、三个修饰符、自定义组件的 v-model。表单是前端开发的高频场景,这一章学透,你能少写一半代码。下一篇我们看 Vue 的核心组织单元——组件。
← 上一篇 Vue 事件处理
下一篇 Vue 组件基础 →