在 Vue 开发中,v-model
是实现 "数据双向绑定" 的核心指令,看似简单却藏着灵活的适配逻辑 ------ 不仅能作用于输入框等原生元素,还能自定义组件的双向绑定规则。本文从基础用法切入,拆解其原理,再到自定义组件适配,帮你彻底吃透v-model
。
一、基础认知:v-model 不是 "魔法",是语法糖
很多人误以为v-model
是 Vue 独有的 "双向绑定魔法",其实它是 **v-bind:value
(单向绑定值)+ v-on:input
(监听输入事件)的语法糖 **,本质是 Vue 帮我们简化了重复代码。
以原生<input>
为例:
<!-- 完整写法 -->
<input
:value="username"
@input="username = $event.target.value"
>
<!-- v-model语法糖(完全等价于上面) -->
<input v-model="username">
- 当用户输入时,
<input>
会触发input
事件,Vue 自动将输入值($event.target.value
)赋值给username
; - 当
username
数据变化时,Vue 通过v-bind:value
自动同步到输入框的 value 属性。
不同原生元素的v-model
,绑定的 "属性" 和 "事件" 略有不同(Vue 已内置适配):
元素类型 | 等价的 v-bind 属性 | 等价的监听事件 | 示例 |
---|---|---|---|
文本输入框(input/text) | value | input | <input v-model="text"> |
复选框(input/checkbox) | checked | change | <input type="checkbox" v-model="isAgree"> |
单选框(input/radio) | checked | change | <input type="radio" v-model="gender"> |
下拉选择器(select) | value | change | <select v-model="city"><option>...</option></select> |
二、关键进阶:自定义组件适配 v-model
当我们封装自定义组件(如 "自定义输入框""数量选择器")时,默认无法直接使用v-model
------ 需要手动告诉组件:"绑定哪个属性?触发哪个事件来更新数据?"
核心原理:组件的 "value-prop" 和 "input-event"
Vue 规定,自定义组件使用v-model
时,默认遵循两个约定:
- 组件接收一个名为
value
的 prop,用于接收父组件传递的值; - 组件内部通过
$emit('input', 新值)
触发事件,父组件会自动将 "新值" 赋值给v-model
绑定的变量。
实战:封装一个 "带加减的数量选择器" 组件
1. 子组件(CountSelector.vue)
<template>
<div class="count-selector">
<!-- 减号按钮:点击时触发input事件,传递当前值-1 -->
<button @click="handleMinus" :disabled="count <= 1">-</button>
<!-- 显示当前数量(从父组件接收的value) -->
<span>{{ count }}</span>
<!-- 加号按钮:点击时触发input事件,传递当前值+1 -->
<button @click="handlePlus">+</button>
</div>
</template>
<script setup>
// 1. 接收父组件通过v-model传递的value(约定名)
const props = defineProps({
value: {
type: Number,
default: 1 // 默认数量为1
}
});
// 2. 定义触发事件的方法(约定触发input事件)
const emit = defineEmits(['input']);
// 减号逻辑:最小为1
const handleMinus = () => {
if (props.value > 1) {
emit('input', props.value - 1); // 触发input事件,传递新值
}
};
// 加号逻辑
const handlePlus = () => {
emit('input', props.value + 1); // 触发input事件,传递新值
};
</script>
<style scoped>
.count-selector {
display: flex;
align-items: center;
gap: 8px;
}
.count-selector button {
padding: 2px 8px;
}
</style>
2. 父组件使用(直接用 v-model)
<template>
<div>
<p>当前选择数量:{{ selectedCount }}</p>
<!-- 自定义组件直接用v-model,和原生input用法一致 -->
<CountSelector v-model="selectedCount" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import CountSelector from './CountSelector.vue';
// 父组件的响应式变量
const selectedCount = ref(1);
</script>
灵活适配:自定义 prop 名和事件名(Vue3 专属)
如果不想用默认的value
和input
(比如组件已用value
做其他用途),Vue3 允许通过model
选项自定义:
<!-- 子组件:在defineProps外添加model选项 -->
<script setup>
// 自定义v-model的prop名(从value改为count)和事件名(从input改为update:count)
defineOptions({
model: {
prop: 'count', // 父组件v-model绑定的值,会传递给count prop
event: 'update:count' // 子组件触发这个事件,父组件会更新v-model
}
});
// 此时接收的prop名是count,不是value
const props = defineProps({
count: {
type: Number,
default: 1
}
});
// 触发的事件名也要改成update:count
const emit = defineEmits(['update:count']);
const handleMinus = () => {
if (props.count > 1) {
emit('update:count', props.count - 1); // 对应自定义的事件名
}
};
</script>
父组件用法不变,依然是<CountSelector v-model="selectedCount" />
------Vue 会自动按子组件定义的model
规则适配。
三、避坑指南:2 个常见错误
-
子组件直接修改 props.value
错误:
handleMinus() { props.value-- }
原因:Vue 中 props 是单向数据流,子组件不能直接修改父组件传递的 prop(会报警告,且破坏数据流向)。
正确:通过
emit('input', 新值)
让父组件修改数据。 -
自定义组件忘记定义 emits
错误:子组件直接
emit('input')
,但没在defineEmits
中声明。原因:Vue3 要求显式声明组件触发的事件(提高代码可维护性),未声明会报警告。
正确:
const emit = defineEmits(['input'])
(或自定义事件名)。
总结
v-model
的核心是 "语法糖 + 约定":
- 原生元素:Vue 已内置
value+input
(或对应属性 / 事件)的适配; - 自定义组件:需遵循 "接收 prop + 触发事件" 的约定,也可灵活自定义 prop 和事件名。
掌握这个逻辑后,无论是封装基础组件,还是复杂的表单组件(如日期选择器、级联选择器),都能轻松实现双向绑定,让代码更简洁易读。