v-model 是 Vue 中最常用的指令之一,它让表单输入和数据状态之间的同步变得极其简单。但它的本质只是一个语法糖,理解了它背后的机制,你就能在组件封装中灵活运用,甚至实现多个双向绑定。
语法糖本质:一个属性绑定加一个事件监听
在原生表单元素上使用 v-model="message",Vue 会根据元素类型自动展开为对应的属性绑定和事件处理:
<!-- 在文本输入框上 -->
<input v-model="message" />
<!-- 等价于 -->
<input
:value="message"
@input="message = $event.target.value"
/>
如果是复选框,它会处理 checked 属性和 change 事件;如果是单选按钮或选择框,也有相应的展开规则。对于开发者来说,v-model 只是一个简洁的写法,省去了手动绑定属性和事件的麻烦。
在组件上使用 v-model
v-model 还可以用在自定义组件上,实现父子组件之间的双向数据流。这是封装表单控件、对话框、开关等组件的核心手段。
默认情况:Vue 3 中,v-model 在组件上的默认展开是:
- prop:
modelValue - 事件:
update:modelValue
也就是说:
<MyComponent v-model="someValue" />
会被展开为:
<MyComponent
:modelValue="someValue"
@update:modelValue="newValue => someValue = newValue"
/>
这就要求子组件内接收名为 modelValue 的 prop,并在需要更新父组件数据时,触发 update:modelValue 事件。
子组件示例:封装一个自定义输入框
<!-- MyInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
父组件使用起来和原生表单元素一样自然:
<MyInput v-model="form.name" />
自定义多个 v-model 绑定
Vue 3 支持为 v-model 指定参数,让你在一个组件上实现多个双向绑定。语法是 v-model:参数名="数据",它会展开为 :参数名="数据" 和 @update:参数名="..."。
典型场景:封装一个带标题和内容的模态框,父组件需要同时控制其显示状态和显示文本。
<!-- 父组件 -->
<MyModal
v-model:visible="isShow"
v-model:title="modalTitle"
/>
子组件需要定义对应的 props 和 emits:
<!-- MyModal.vue -->
<template>
<div v-if="visible" class="modal">
<h2>{{ title }}</h2>
<button @click="$emit('update:visible', false)">关闭</button>
</div>
</template>
<script setup>
defineProps({
visible: Boolean,
title: String
})
defineEmits(['update:visible', 'update:title'])
</script>
注意:不带参数的 v-model="data" 依然是 modelValue,与 v-model:modelValue 完全等价。
自定义修饰符
v-model 还能配合修饰符使用,比如 .trim、.number、.lazy 是内置的。在组件上,你还可以自定义修饰符,让组件感知到父组件添加的修饰符并做出相应处理。
修饰符会通过 prop 传递:对于默认的 v-model,修饰符所在 prop 名为 modelModifiers;对于带参数的 v-model:title,修饰符 prop 名为 titleModifiers。
示例:自定义输入框,支持一个 capitalize 修饰符,自动将输入内容首字母大写。
<!-- MyInput.vue -->
<template>
<input :value="modelValue" @input="handleInput" />
</template>
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { type: Object, default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
function handleInput(e) {
let value = e.target.value
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
父组件使用:
<MyInput v-model.capitalize="text" />
当 capitalize 修饰符存在时,子组件就会在触发更新前自动转换数据。
v-model 与表单校验结合
在实际项目中,v-model 经常与表单校验库(如 VeeValidate 或组件库内置校验)结合使用。这时双向绑定仍然是最基础的数据通道,校验逻辑通过规则对象或函数来附加,并不会破坏 v-model 的简洁性。
总结:v-model 只是一层语法糖,但理解了它的本质(prop + event)之后,你可以在组件封装中创造出极为顺滑的双向绑定体验。多个 v-model 和自定义修饰符让组件 API 更加语义化,减少了冗余的 prop 和事件声明,这正是 Vue 3 在组件通信上的一大改进。