讲解

表单是「用户输入」和「应用状态」的交汇点。传统写法要手动给 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。