前言
在 Vue 开发中,父子组件之间的数据通信是一个核心话题。v-model 作为 Vue 的双向绑定指令,看似简单,实则蕴含着强大的表达能力。很多开发者对 v-model 的理解停留在"表单输入绑定"的层面,殊不知它早已进化为处理复杂父子组件通信的利器。
本文将深入剖析 v-model 的本质,从基础用法到进阶技巧,再到实战案例,帮助我们掌握这一强大的通信工具。
v-model 的本质
语法糖::modelValue + @update:modelValue
v-model 的本质其实是一个语法糖。在 Vue3 中,下面这两种写法是完全等价的:
html
<!-- 这种写法 -->
<ChildComponent v-model="parentData" />
<!-- 等价于这种写法 -->
<ChildComponent
:modelValue="parentData"
@update:modelValue="parentData = $event"
/>
双向绑定的实现原理
v-model 实现双向绑定的核心是 Props 向下传递,Events 向上传递: 
双向绑定的具体流程
- 父组件通过
:modelValue将数据传递给子组件 - 子组件通过
props.modelValue接收数据并展示 - 当子组件内部需要修改数据时,通过
emit('update:modelValue', newValue)通知父组件 - 父组件监听到事件后更新自己的数据
- 父组件数据更新后,再次通过
Props传递给子组件,完成闭环
从 Vue2 的 v-bind.sync 到 Vu3 的 v-model
如果我们想在 Vue2 中处理多个双向绑定需要使用 .sync 修饰符:
html
<!-- Vue 2 中的 .sync -->
<ChildComponent
:name.sync="userName"
:age.sync="userAge"
/>
<!-- 等价于 -->
<ChildComponent
:name="userName"
@update:name="userName = $event"
:age="userAge"
@update:age="userAge = $event"
/>
而在Vue 3 统一为 v-model 语法,更加直观:
html
<!-- Vue 3 中的多 v-model -->
<ChildComponent
v-model:name="userName"
v-model:age="userAge"
/>
v-model 基础用法回顾
自定义组件支持 v-model
如果要让一个自定义组件支持 v-model,需要做两件事:
- 接收
modelValue:默认名称 - 当值变化时,触发
update:modelValue事件
html
<!-- 自定义输入框组件 CustomInput.vue -->
<template>
<div class="custom-input">
<input
:value="modelValue"
@input="handleInput"
/>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
function handleInput(e: Event) {
const value = (e.target as HTMLInputElement).value
emit('update:modelValue', value)
}
</script>
<!-- 使用方式 -->
<template>
<CustomInput
v-model="searchText"
/>
</template>
默认 prop 和事件名称
v-model 的默认配置:
- Prop 名称:
modelValue - 事件名称:
update:modelValue
当然,我们也可以通过修改 v-model 的参数来改变这些名称:
html
<!-- 指定参数名 -->
<ChildComponent v-model:title="pageTitle" />
<!-- 等价于 -->
<ChildComponent
:title="pageTitle"
@update:title="pageTitle = $event"
/>
多个 v-model 绑定
场景:一个组件需要双向绑定多个值
想象一下:在用户表单组件中,我们需要同时绑定姓名、年龄、邮箱等多个值:
html
<!-- 父组件 -->
<template>
<UserForm
v-model:name="userName"
v-model:age="userAge"
v-model:email="userEmail"
@submit="handleSubmit"
/>
</template>
<script setup>
import { ref } from 'vue'
import UserForm from './UserForm.vue'
const userName = ref('张三')
const userAge = ref(25)
const userEmail = ref('zhangsan@example.com')
function handleSubmit() {
console.log('提交表单', {
name: userName.value,
age: userAge.value,
email: userEmail.value
})
}
</script>
实现:指定不同的参数名
html
<!-- UserForm.vue -->
<template>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label>姓名</label>
<input
:value="name"
@input="$emit('update:name', $event.target.value)"
/>
</div>
<div class="form-group">
<label>年龄</label>
<input
type="number"
:value="age"
@input="$emit('update:age', Number($event.target.value))"
/>
</div>
<div class="form-group">
<label>邮箱</label>
<input
:value="email"
@input="$emit('update:email', $event.target.value)"
type="email"
/>
</div>
<button type="submit">提交</button>
</form>
</template>
<script setup lang="ts">
defineProps<{
name: string
age: number
email: string
}>()
const emit = defineEmits<{
'update:name': [value: string]
'update:age': [value: number]
'update:email': [value: string]
'submit': []
}>()
function handleSubmit() {
emit('submit')
}
</script>
复杂数据结构的双向绑定
除了简单的基础类型数据的双向绑定外,有时候我们也需要双向绑定一个复杂对象:
html
<template>
<AddressEditor v-model:address="userAddress" />
</template>
<script setup>
import { ref } from 'vue'
interface Address {
province: string
city: string
district: string
detail: string
zipCode?: string
}
const userAddress = ref<Address>({
province: '广东省',
city: '深圳市',
district: '南山区',
detail: '科技园路1号'
})
</script>
这其实相当于:
html
<template>
<div class="address-editor">
<div class="address-item">
<label>省份</label>
<input
:value="address.province"
@input="updateAddress('province', $event.target.value)"
/>
</div>
<div class="address-item">
<label>城市</label>
<input
:value="address.city"
@input="updateAddress('city', $event.target.value)"
/>
</div>
<div class="address-item">
<label>区县</label>
<input
:value="address.district"
@input="updateAddress('district', $event.target.value)"
/>
</div>
<div class="address-item">
<label>详细地址</label>
<input
:value="address.detail"
@input="updateAddress('detail', $event.target.value)"
/>
</div>
<div class="address-item">
<label>邮编</label>
<input
:value="address.zipCode"
@input="updateAddress('zipCode', $event.target.value)"
/>
</div>
</div>
</template>
<script setup lang="ts">
interface Address {
province: string
city: string
district: string
detail: string
zipCode?: string
}
const props = defineProps<{
address: Address
}>()
const emit = defineEmits<{
'update:address': [value: Address]
}>()
function updateAddress<K extends keyof Address>(key: K, value: Address[K]) {
emit('update:address', {
...props.address,
[key]: value
})
}
</script>
自定义 v-model 修饰符
内置修饰符的作用
| 修饰符 | 作用 | 适用场景 |
|---|---|---|
.trim |
自动过滤用户输入的首尾空白字符 | 用户名、留言内容等不需要首尾空格的文本输入 |
.number |
将用户输入自动转换为数值类型 | 年龄、数量等数字类型的输入 |
.lazy |
将默认的 input 事件改为 change 事件触发同步 |
减少频繁更新,适合评论框等场景 |
内置修饰符的处理
在自定义组件中需要手动处理这些修饰符:
html
<template>
<CustomInput
v-model.trim="text" <!-- 自动去除首尾空格 -->
v-model.number="age" <!-- 自动转换为数字类型 -->
v-model.lazy="comment" <!-- 失焦后才更新 -->
/>
</template>
在自定义组件中处理这些修饰符
html
<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="handleInput"
@change="handleChange"
/>
</template>
<script setup>
const props = defineProps<{
modelValue: string | number
modelModifiers?: {
trim?: boolean
number?: boolean
lazy?: boolean
}
}>()
const emit = defineEmits(['update:modelValue'])
function handleInput(e: Event) {
if (props.modelModifiers?.lazy) {
// lazy 模式下,只在 change 事件触发
return
}
let value = (e.target as HTMLInputElement).value
// 处理 trim 修饰符
if (props.modelModifiers?.trim) {
value = value.trim()
}
// 处理 number 修饰符
if (props.modelModifiers?.number) {
const num = parseFloat(value)
value = isNaN(num) ? value : num
}
emit('update:modelValue', value)
}
function handleChange(e: Event) {
if (!props.modelModifiers?.lazy) {
return
}
let value = (e.target as HTMLInputElement).value
if (props.modelModifiers?.trim) {
value = value.trim()
}
if (props.modelModifiers?.number) {
const num = parseFloat(value)
value = isNaN(num) ? value : num
}
emit('update:modelValue', value)
}
</script>
常见陷阱与解决方案
不要直接修改 props
这是新手最常见的错误:
html
<!-- ❌ 错误:直接修改 props -->
<template>
<input v-model="modelValue" />
</template>
<script setup>
defineProps<{
modelValue: string
}>()
</script>
解决方案:通过事件通知父组件
html
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
处理非字符串类型的 v-model
对于数字、布尔值等类型,我们在使用时需要特别注意类型转换:
html
<template>
<!-- ✅ 正确处理数字类型 -->
<input
type="number"
:value="modelValue"
@input="handleNumberInput"
/>
</template>
<script setup>
const props = defineProps<{
modelValue: number
}>()
const emit = defineEmits(['update:modelValue'])
function handleNumberInput(e: Event) {
const value = (e.target as HTMLInputElement).value
// 转换为数字,处理空值
const num = value === '' ? 0 : Number(value)
emit('update:modelValue', num)
}
</script>
v-model 与响应式数据的配合
当使用对象作为 v-model 的值时,一定注意响应式丢失的问题:
html
<template>
<!-- 这种情况没问题 -->
<ChildComponent v-model="user" />
<!-- 但这种情况会导致响应式丢失! -->
<ChildComponent
v-model="user.name"
v-model="user.age"
/>
</template>
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: '张三',
age: 25
})
// ❌ 这样使用 v-model 会破坏响应式
</script>
解决方案:使用 ref
html
<script setup>
import { ref } from 'vue'
const user = ref({
name: '张三',
age: 25
})
</script>
<template>
<ChildComponent
v-model:name="user.value.name"
v-model:age="user.value.age"
/>
</template>
处理异步更新
有时需要在值变化后执行某些操作,但需要注意 Vue 的异步更新机制:
html
<script setup>
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits(['update:modelValue'])
function handleInput(e: Event) {
const value = e.target.value
emit('update:modelValue', value)
// ❌ 这里的 props.modelValue 还是旧值
console.log(props.modelValue)
// ✅ 使用 nextTick 获取更新后的值
import { nextTick } from 'vue'
nextTick(() => {
console.log(props.modelValue) // 现在是最新值
})
}
</script>
最佳实践清单
- 优先使用多个
v-model而不是一个包含多个字段的对象 - 为所有
v-model定义 TypeScript 类型,包括修饰符 - 不要直接修改
props,始终通过事件更新 - 处理非字符串类型时做好类型转换
- 提供合理的默认值和空状态处理
- 考虑使用计算属性实现复杂的转换逻辑
- 为组件暴露
reset等方法,方便父组件控制 - 使用
v-model修饰符实现可复用的输入处理逻辑
结语
好的组件设计应该是使用者友好型 。当我们设计的组件让其他开发者或使用者,只需要写 v-model 就能完成复杂的双向绑定,那我们就真正掌握了 v-model 的精髓。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!