Vue 中 v-model 的原理和自定义 v-model 怎么用?

Vue 中 v-model 的原理和自定义 v-model 怎么用?

一句话总结:v-model 本质是「:value+@input」的语法糖,自定义组件用modelValue+update:modelValue即可实现双向绑定,多个 v-model 用具名参数搞定!


正文目录

  1. v-model 到底做了什么?
  2. 5 大高频踩坑场景 & 修复代码
  3. 万能兜底:props + emit + computed 代理
  4. 预防 checklist(不再踩坑)
  5. 一句话总结

一、v-model 到底做了什么?

原生表单元素上用 v-model:

<template> <input v-model="msg" /> </template>

等价于:

<template> <input :value="msg" @input="msg = $event.target.value" /> </template>

核心:v-model = 绑定 value + 监听 input 事件。这不是魔法,是语法糖。

自定义组件上用 v-model(Vue 3):

<!-- 父组件 --> <template> <MyInput v-model="msg" /> </template>

等价于:

<template> <MyInput :modelValue="msg" @update:modelValue="msg = $event" /> </template>

Vue 3 的变化:默认 prop 名从value改为modelValue,事件名从input改为update:modelValue


二、5 大高频踩坑场景 & 修复代码

① 自定义组件直接修改 modelValue
<!-- ❌ 子组件直接改 prop,触发 Avoid mutating a prop 警告 --> <script setup> const props = defineProps(['modelValue']); const updateText = (e) => { props.modelValue = e.target.value; // ❌ 直接改 prop }; </script> <template> <input :value="modelValue" @input="updateText" /> </template>

修复:用 emit 通知父组件

<!-- ✅ 子组件用 emit 通知父组件修改 --> <script setup> const props = defineProps(['modelValue']); const emit = defineEmits(['update:modelValue']); const updateText = (e) => { emit('update:modelValue', e.target.value); // ✅ emit 通知 }; </script> <template> <input :value="modelValue" @input="updateText" /> </template>
② 用 v-model 绑定对象的属性,父组件不更新
<!-- ❌ 子组件修改了对象属性,但父组件的 v-model 没反应 --> <script setup> const props = defineProps(['modelValue']); const handleClick = () => { props.modelValue.name = 'new name'; // ❌ 直接改对象属性,不触发 update }; </script>

修复:emit 一个新对象

<!-- ✅ emit 新对象,触发响应式更新 --> <script setup> const props = defineProps(['modelValue']); const emit = defineEmits(['update:modelValue']); const handleClick = () => { emit('update:modelValue', { ...props.modelValue, name: 'new name' }); // ✅ 新对象 }; </script>
③ 一个组件需要多个 v-model
<!-- ❌ 只有一个 v-model,其他属性只能用 prop + emit 手动传 --> <template> <UserForm v-model="user" :name="name" @update:name="name = $event" /> </template>

修复:Vue 3 支持多个具名 v-model

<!-- ✅ 多个 v-model 用具名参数 --> <template> <UserForm v-model:name="name" v-model:age="age" v-model:email="email" /> </template>
<!-- 子组件 --> <script setup> defineProps(['name', 'age', 'email']); defineEmits(['update:name', 'update:age', 'update:email']); </script>
④ v-model 绑定到computed,但没写 setter
<script setup> import { computed } from 'vue'; const props = defineProps(['modelValue']); const emit = defineEmits(['update:modelValue']); // ❌ 只读 computed 没有 setter,v-model 写入时报错 const proxy = computed(() => { return props.modelValue.toUpperCase(); }); </script> <template> <input v-model="proxy" /> <!-- ❌ 没有 setter,无法写入 --> </template>

修复:computed 写 get + set

<script setup> import { computed } from 'vue'; const props = defineProps(['modelValue']); const emit = defineEmits(['update:modelValue']); // ✅ computed 带 get/set,代理 v-model const proxy = computed({ get() { return props.modelValue; }, set(val) { emit('update:modelValue', val); } }); </script> <template> <input v-model="proxy" /> <!-- ✅ 读写都通 --> </template>
⑤ 修饰符 .trim / .number / .lazy 在自定义组件上不生效
<!-- ❌ 父组件用了修饰符,但子组件没处理 --> <template> <MyInput v-model.trim="msg" /> <!-- .trim 没有效果 --> </template>

修复:子组件接收 modelModifiers 并处理

<!-- ✅ 子组件处理修饰符 --> <script setup> const props = defineProps({ modelValue: String, modelModifiers: { default: () => ({}) } // ✅ 接收修饰符对象 }); const emit = defineEmits(['update:modelValue']); const handleInput = (e) => { let val = e.target.value; if (props.modelModifiers.trim) { val = val.trim(); // ✅ 手动处理 .trim } emit('update:modelValue', val); }; </script> <template> <input :value="modelValue" @input="handleInput" /> </template>

三、万能兜底:props + emit + computed 代理

场景方案关键点
基本双向绑定defineProps+defineEmitsprop 只读,emit 通知
需要转换值computed get/setget 读 prop,set emit
多个双向绑定具名 v-modelv-model:xxx
对象属性绑定emit 新对象{ ...old, key: newVal }
修饰符处理modelModifiers手动 trim/number/lazy

万能模板

<!-- 父组件 --> <script setup> import { ref } from 'vue'; import CustomInput from './CustomInput.vue'; const msg = ref(''); const user = ref({ name: '', age: 0 }); </script> <template> <!-- 基本用法 --> <CustomInput v-model.trim="msg" /> <!-- 对象用法 --> <CustomForm v-model="user" /> </template>
<!-- 子组件 CustomInput.vue --> <script setup> import { computed } from 'vue'; const props = defineProps({ modelValue: String, modelModifiers: { default: () => ({}) } }); const emit = defineEmits(['update:modelValue']); // ✅ computed 代理,处理修饰符 const proxy = computed({ get() { return props.modelValue; }, set(val) { if (props.modelModifiers.trim) val = val.trim(); emit('update:modelValue', val); } }); </script> <template> <input v-model="proxy" /> </template>

四、预防 checklist

  • 自定义组件 v-model永远不要直接改 modelValue prop,用 emit
  • 需要转换值时用computed get/set代理,别手动写 input 事件
  • 多个双向绑定用具名 v-modelv-model:xxx),不要叠一堆 prop + emit
  • 绑定对象属性时emit 新对象,不要直接改原对象属性
  • 修饰符在自定义组件上需要手动处理,子组件接收modelModifiers
  • Vue 2 和 Vue 3 的 v-modelprop 名不同(value vs modelValue),迁移注意
  • 表单组件封装时,先写 computed 代理再绑 v-model,一步到位

五、一句话总结

「v-model =:value+@input的语法糖」,自定义组件用modelValue+update:modelValue实现双向绑定,需要转换值就上 computed get/set,多个绑定就用具名 v-model。记住:prop 只读,永远 emit!


最后问候亲爱的朋友们,并邀请你们阅读我的全新著作

📚 《Vue.js 3企业级项目开发实战(微课视频版》