v-model 与各表单控件
表单与 v-model · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
v-model 是 Vue 表单的语法糖。这一节要掌握它在文本框、复选框、单选、下拉选择上的不同行为,理解修饰符 .number / .trim / .lazy 的作用,并能说清它背后展开成的 :value + @input 事件链。
问题场景
一个设置表单:昵称输入框、接收通知的复选框、主题单选、语言下拉。用原生 JS 要写四套事件监听,用 React 要写四组 value/onChange。Vue 里看起来都能用 v-model 一把梭,但细节差异很快暴露:复选框绑数组和绑布尔行为不同;下拉多选要绑数组;数字输入框拿到的是字符串,做加法变成拼接。这些不是 API 背不熟,是没弄清 v-model 在每类控件上的展开形式。
核心解释
v-model 的本质:把「值的绑定」和「变化事件」合成一条双向通道。展开形式随控件类型不同:
<script setup>
import { reactive } from "vue";
const form = reactive({
nickname: "",
bio: "",
subscribe: true, // 单个复选框 → 布尔
hobbies: [], // 多个复选框 → 数组
theme: "light", // 单选 → 字符串
lang: "zh", // 单选下拉 → 字符串
tags: [], // 多选下拉 → 数组
});
const hobbies = ["阅读", "骑行", "摄影"];
const langs = [
{ value: "zh", label: "中文" },
{ value: "en", label: "English" },
];
const tagOptions = ["vue", "react", "ts"];
</script>
<template>
<form>
<label>昵称 <input v-model="form.nickname" /></label>
<label>简介 <textarea v-model="form.bio"></textarea></label>
<label><input type="checkbox" v-model="form.subscribe" /> 接收通知</label>
<fieldset>
<legend>爱好</legend>
<label v-for="h in hobbies" :key="h">
<input type="checkbox" v-model="form.hobbies" :value="h" /> {{ h }}
</label>
</fieldset>
<fieldset>
<legend>主题</legend>
<label><input type="radio" value="light" v-model="form.theme" /> 浅色</label>
<label><input type="radio" value="dark" v-model="form.theme" /> 深色</label>
</fieldset>
<label>
语言
<select v-model="form.lang">
<option v-for="l in langs" :key="l.value" :value="l.value">{{ l.label }}</option>
</select>
</label>
<label>
标签(多选)
<select v-model="form.tags" multiple>
<option v-for="t in tagOptions" :key="t" :value="t">{{ t }}</option>
</select>
</label>
</form>
</template>
展开规则记忆表:
- 文本框/textarea:
:value="x"+@input="x = $event.target.value" - 单个复选框:
:checked="x"+@change="x = $event.target.checked" - 多个复选框:
:checked="x.includes(v)"+@change里增删数组 - 单选:
:checked="x === v"+@change="x = v" - select:
:value="x"+@change
修饰符解决三类常见需求:
<template>
<!-- .number:自动把输入转成数字(type="number" 时也建议加) -->
<input v-model.number="form.age" type="number" />
<!-- .trim:去掉首尾空格 -->
<input v-model.trim="form.nickname" />
<!-- .lazy:从 input 事件改为 change 事件(失焦/回车才同步) -->
<input v-model.lazy="form.nickname" />
<!-- 修饰符可串联 -->
<input v-model.trim.number="form.age" />
</template>
.number 的边界:输入非数字时回退为原始字符串,不会变成 NaN。所以后端提交前仍要做类型校验。
示例拆解
一个可观察 v-model 展开过程的实验。把 v-model 手动展开成等价的绑定,两者行为完全一致:
<script setup>
import { ref } from "vue";
const text = ref("hello");
const manual = ref("hello");
</script>
<template>
<p>v-model:<input v-model="text" /></p>
<p>手动展开:<input :value="manual" @input="manual = ($event.target as HTMLInputElement).value" /></p>
<p>text = {{ text }};manual = {{ manual }}</p>
</template>
输入任意内容,两个输入框和下方文本同步变化。这个实验的价值:当你遇到「v-model 不生效」的组件时,知道该检查哪两个环节——值有没有绑上(:value),事件有没有接住(@input)。对自定义组件,就是 props 的 modelValue 和 emit 的 update:modelValue(V04 讲过)。
另一个常见需求:输入框实时格式化(比如手机号每 4 位加空格)。做法是用 computed 的 get/set 包一层,或 watch 后写回:
<script setup>
import { ref, watch } from "vue";
const raw = ref("");
watch(raw, (v) => {
const digits = v.replace(/\D/g, "").slice(0, 11);
raw.value = digits.replace(/(\d{4})(?=\d)/g, "$1 ");
});
</script>
<template>
<input v-model="raw" placeholder="手机号" />
</template>
注意这里 watch 里写回 raw 会再次触发 watch,但因为格式化后的值再次格式化不变(幂等),不会无限循环。如果格式化不幂等,要加判断。
动手任务
- 把上面的设置表单跑通,逐个验证复选框、单选、下拉的数据类型(用
JSON.stringify(form)显示在页面上)。 - 给「昵称」加
.trim,输入前后空格后提交,确认存储的是干净值。 - 给年龄输入框加
.number,输入abc后观察值类型,解释为什么需要提交前再校验。
常见错误
- 数字输入框忘记
.number:"1" + 1 === "11",小计计算错。加.number或在计算时Number()。 - 多选框绑了布尔却期待数组:
v-model="form.hobbies"且初始值是false时,勾选后变成true而不是数组。初始值必须是[]。 v-model绑定 computed 但没有 setter:只读 computed 上写 v-model 会报「Write operation failed」。用可写 computed 或改成事件绑定。.lazy用在需要实时反馈的场景:输入时不同步,只有失焦才更新,实时字数统计会滞后。
检查与复盘
- 能默写 v-model 在文本框、单个复选框、多选框、单选上的展开形式
- 能说出三个修饰符的作用和适用场景
- 能诊断「v-model 不生效」时该检查的两个环节
参考资料
- Vue 官方文档「表单输入绑定」:全部控件类型与修饰符
- Vue 官方文档「组件上的 v-model」:自定义组件的 v-model 实现
读完这一课了?
标记完成后,工作台会安排接下来的学习。