讲解
表单是「用户输入」和「应用状态」的交汇点。传统写法要手动给 input 绑 value、监听 input 事件、再写回状态,每个控件三行样板。v-model 把这三步压成一个指令:<input v-model="name"> 之后,输入框显示永远等于 name 的值,用户输入也自动写回 name——这就是双向绑定。
重要的是看穿语法糖:v-model="name" 等价于 :value="name" @input="name = $event.target.value"。理解了这层展开,所有「v-model 不工作了」的问题都有答案——比如对 checkbox 它绑的是 checked 而不是 value,对 radio 配合 value 属性区分选项,对 select 绑的是选中项的 value。修饰符也很实用:.trim 自动去首尾空格,.number 把输入转数字(转不动就保留原样),.lazy 把同步时机从 input 推迟到 change(失焦才更新)。
多值控件的行为要记准:单个 checkbox 绑布尔;多个 checkbox 绑同一个数组,勾选会把对应 value 推进数组;radio 组绑同一个变量;select 加 multiple 后绑数组。文件上传是例外——file input 不能用 v-model(安全限制,JS 不能设文件路径),要用 @change 拿 e.target.files。
示例
把 v-model 的展开式和多 checkbox 的数组合并用 JS 验证一遍(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// v-model="name" ⇔ :value="name" + @input="name = e.target.value"
function createModel(initial) {
const state = { value: initial };
return {
bind: () => ({ value: state.value }),
onInput: (e) => (state.value = e.target.value),
state,
};
}
const name = createModel('');
name.onInput({ target: { value: ' 小明 ' } });
assert.strictEqual(name.bind().value, ' 小明 ');
// .trim 修饰符:写回时去空格
function trim(s) {
return s.trim();
}
assert.strictEqual(trim(name.state.value), '小明');
// .number:能转则转,转不动保留原样
function toNumber(s) {
const n = parseFloat(s);
return Number.isNaN(n) ? s : n;
}
assert.strictEqual(toNumber('42'), 42);
assert.strictEqual(toNumber('abc'), 'abc');
// 多 checkbox:勾选推进数组,取消从数组移除
const picked = [];
function toggle(list, value, checked) {
const i = list.indexOf(value);
if (checked && i === -1) list.push(value);
if (!checked && i !== -1) list.splice(i, 1);
}
toggle(picked, '苹果', true);
toggle(picked, '香蕉', true);
toggle(picked, '苹果', false);
assert.deepStrictEqual(picked, ['香蕉']);
console.log('输入写回:', JSON.stringify(name.state.value), '→ trim 后:', trim(name.state.value));
console.log('.number 转换: "42" →', toNumber('42'), ', "abc" →', JSON.stringify(toNumber('abc')));
console.log('多选结果:', picked.join('、'));
一个完整的迷你表单:
<script setup>
import { ref, reactive } from 'vue';
const form = reactive({ name: '', age: null, fruits: [], agree: false });
function submit() {
console.log('提交:', JSON.stringify(form));
}
</script>
<template>
<form @submit.prevent="submit">
<input v-model.trim="form.name" placeholder="姓名" />
<input v-model.number="form.age" type="number" placeholder="年龄" />
<label><input type="checkbox" value="苹果" v-model="form.fruits" />苹果</label>
<label><input type="checkbox" value="香蕉" v-model="form.fruits" />香蕉</label>
<label><input type="checkbox" v-model="form.agree" />同意协议</label>
<button :disabled="!form.agree">提交</button>
</form>
</template>
常见坑
- 以为 v-model 是黑魔法:忘了它是 value + input 的糖,排查问题时先展开想一遍数据流。
- file input 用 v-model:安全限制下无效,用 @change 读 e.target.files。
- checkbox 值类型混乱:单选绑布尔、多选绑数组,混用会出现「勾了但状态没变」。
- .number 以为转不了会变 0:转不动时保留原字符串,做数字比较前注意类型。
- v-model 套在只读数据上:给 props 或 computed(无 setter)直接 v-model 会报警告,需要中转一层。
小结
v-model = :value + @input 的语法糖;不同控件绑不同的属性;.trim/.number/.lazy 三个修饰符最常用。下一章把 UI 拆成组件:props 与 emits。