本文详细介绍 Vue 3 中实现组件双向绑定的两种方式,帮助你深入理解 v-model 的工作原理。
一、什么是 v-model?
v-model 是 Vue 提供的语法糖,用于实现父子组件之间的双向数据绑定。在 Vue 3 中,v-model 的底层实现发生了变化。
Vue 2 vs Vue 3
Vue 2:
vue
<!-- 父组件 -->
<CustomInput v-model="message" />
<!-- 等价于 -->
<CustomInput
:value="message"
@input="message = $event"
/>
Vue 3:
vue
<!-- 父组件 -->
<CustomInput v-model="message" />
<!-- 等价于 -->
<CustomInput
:modelValue="message"
@update:modelValue="message = $event"
/>
关键变化:
- prop 名从
value改为modelValue - 事件名从
input改为update:modelValue
二、方法一:手动实现(update:modelValue)
这是传统方式,需要手动定义 props 和 emits。
2.1 基础示例
子组件 (CustomInput.vue):
vue
<template>
<div class="custom-input">
<input
:value="modelValue"
@input="handleInput"
placeholder="请输入内容"
/>
</div>
</template>
<script setup>
// 1. 定义 props,接收父组件传入的值
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
// 2. 定义 emits,声明要触发的事件
const emit = defineEmits(['update:modelValue'])
// 3. 处理输入事件,触发更新
const handleInput = (event) => {
emit('update:modelValue', event.target.value)
}
</script>
父组件:
vue
<template>
<div>
<CustomInput v-model="message" />
<p>输入的内容:{{ message }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const message = ref('Hello')
</script>
2.2 工作原理
- 父组件传值 :通过
:modelValue="message"将数据传给子组件 - 子组件接收 :通过
props.modelValue接收数据 - 子组件更新 :通过
emit('update:modelValue', newValue)通知父组件 - 父组件响应 :自动执行
message = newValue
2.3 完整示例:自定义选择器
vue
<template>
<div class="custom-select">
<div
v-for="option in options"
:key="option.value"
class="option"
:class="{ active: modelValue === option.value }"
@click="handleSelect(option.value)"
>
{{ option.label }}
</div>
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {
type: [String, Number],
default: null
},
options: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const handleSelect = (value) => {
// 触发 v-model 更新
emit('update:modelValue', value)
// 触发额外的 change 事件
emit('change', value)
}
</script>
<style scoped>
.option {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ddd;
}
.option.active {
background: #409eff;
color: white;
}
</style>
使用方式:
vue
<template>
<CustomSelect
v-model="selectedValue"
:options="options"
@change="handleChange"
/>
</template>
<script setup>
import { ref } from 'vue'
const selectedValue = ref('option1')
const options = [
{ label: '选项1', value: 'option1' },
{ label: '选项2', value: 'option2' },
{ label: '选项3', value: 'option3' }
]
const handleChange = (value) => {
console.log('选中了:', value)
}
</script>
三、方法二:defineModel(Vue 3.3+)
Vue 3.3 引入的新特性,大幅简化双向绑定的实现。
3.1 基础用法
子组件 (CustomInput.vue):
vue
<template>
<div class="custom-input">
<input
v-model="model"
placeholder="请输入内容"
/>
</div>
</template>
<script setup>
// 一行代码搞定!
const model = defineModel()
</script>
父组件:
vue
<template>
<CustomInput v-model="message" />
</template>
<script setup>
import { ref } from 'vue'
const message = ref('Hello')
</script>
3.2 defineModel 的优势
| 特性 | 手动实现 | defineModel |
|---|---|---|
| 代码量 | 需要 props + emit | 一行代码 |
| 类型定义 | 手动定义 | 自动推导 |
| 可读性 | 较复杂 | 简洁明了 |
| 直接修改 | 不可以 | 可以 |
3.3 带选项的 defineModel
vue
<template>
<input
v-model="model"
type="number"
/>
</template>
<script setup>
// 定义类型、默认值、验证器
const model = defineModel({
type: Number,
default: 0,
required: false,
validator: (value) => value >= 0
})
// 可以直接修改
const increment = () => {
model.value++
}
</script>
3.4 多个 v-model
Vue 3 支持多个 v-model,非常适合复杂表单组件。
子组件 (UserForm.vue):
vue
<template>
<div class="user-form">
<input v-model="name" placeholder="姓名" />
<input v-model="email" placeholder="邮箱" />
<input v-model="age" type="number" placeholder="年龄" />
</div>
</template>
<script setup>
// 定义多个 model
const name = defineModel('name', { type: String, default: '' })
const email = defineModel('email', { type: String, default: '' })
const age = defineModel('age', { type: Number, default: 0 })
</script>
父组件:
vue
<template>
<UserForm
v-model:name="userName"
v-model:email="userEmail"
v-model:age="userAge"
/>
<div>
<p>姓名: {{ userName }}</p>
<p>邮箱: {{ userEmail }}</p>
<p>年龄: {{ userAge }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const userName = ref('张三')
const userEmail = ref('zhangsan@example.com')
const userAge = ref(25)
</script>
四、实战案例:类型选择器
基于你的项目代码,这是一个实际的应用示例。
4.1 使用 update:modelValue 实现
vue
<template>
<div class="dict-select">
<span v-if="label" class="filter-label">{{ label }}</span>
<div class="filter-options">
<span
v-for="item in dictOptions"
:key="item.value"
class="filter-item"
:class="{ 'active': String(modelValue) === String(item.value) }"
@click="handleSelect(item.value)"
>
{{ item.label }}
</span>
<span
class="filter-item"
:class="{ 'active': modelValue === null }"
@click="handleSelect(null)"
>
全部
</span>
</div>
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {
type: [String, Number],
default: null
},
dictData: {
type: Array,
default: () => []
},
label: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const dictOptions = computed(() => {
return props.dictData || []
})
const handleSelect = (value) => {
emit('update:modelValue', value)
emit('change', value)
}
</script>
使用:
vue
<template>
<DictSelect
v-model="queryParams.invoiceType"
:dictData="finance_invoice_category"
label="类型"
@change="handleInvoiceTypeChange"
/>
</template>
<script setup>
const queryParams = ref({
invoiceType: null
})
const handleInvoiceTypeChange = (value) => {
console.log('类型改变:', value)
// 重新加载数据
loadData()
}
</script>
4.2 使用 defineModel 重构
vue
<template>
<div class="dict-select">
<span v-if="label" class="filter-label">{{ label }}</span>
<div class="filter-options">
<span
v-for="item in dictData"
:key="item.value"
class="filter-item"
:class="{ 'active': String(model) === String(item.value) }"
@click="handleSelect(item.value)"
>
{{ item.label }}
</span>
<span
class="filter-item"
:class="{ 'active': model === null }"
@click="handleSelect(null)"
>
全部
</span>
</div>
</div>
</template>
<script setup>
// 使用 defineModel 简化代码
const model = defineModel({
type: [String, Number],
default: null
})
defineProps({
dictData: {
type: Array,
default: () => []
},
label: {
type: String,
default: ''
}
})
const emit = defineEmits(['change'])
const handleSelect = (value) => {
model.value = value // 直接修改,自动触发更新
emit('change', value)
}
</script>
五、常见问题与解决方案
5.1 为什么不能直接修改 props?
vue
<!-- ❌ 错误做法 -->
<script setup>
const props = defineProps({
modelValue: String
})
// 这样会报错!
const handleInput = (e) => {
props.modelValue = e.target.value // ❌ 不允许
}
</script>
<!-- ✅ 正确做法 -->
<script setup>
const props = defineProps({
modelValue: String
})
const emit = defineEmits(['update:modelValue'])
const handleInput = (e) => {
emit('update:modelValue', e.target.value) // ✅ 通过事件通知父组件
}
</script>
原因: Vue 遵循单向数据流原则,子组件不能直接修改父组件的数据。
5.2 如何在子组件中使用 v-model?
vue
<template>
<!-- ❌ 错误:不能直接对 prop 使用 v-model -->
<input v-model="modelValue" />
</template>
<script setup>
const props = defineProps({
modelValue: String
})
</script>
解决方案 1:使用 :value 和 @input
vue
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
解决方案 2:使用 computed
vue
<template>
<input v-model="localValue" />
</template>
<script setup>
const props = defineProps({
modelValue: String
})
const emit = defineEmits(['update:modelValue'])
const localValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
</script>
解决方案 3:使用 defineModel(推荐)
vue
<template>
<input v-model="model" />
</template>
<script setup>
const model = defineModel()
</script>
5.3 类型转换问题
vue
<script setup>
const props = defineProps({
modelValue: {
type: Number, // 期望是数字
default: 0
}
})
const emit = defineEmits(['update:modelValue'])
const handleInput = (e) => {
// input 的 value 是字符串,需要转换
const value = parseFloat(e.target.value) || 0
emit('update:modelValue', value)
}
</script>
5.4 对象和数组的双向绑定
vue
<template>
<div>
<input v-model="localForm.name" />
<input v-model="localForm.age" type="number" />
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue'])
// 使用 computed 处理对象
const localForm = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
// 或者使用 watch 深度监听
const localForm = ref({ ...props.modelValue })
watch(localForm, (newVal) => {
emit('update:modelValue', newVal)
}, { deep: true })
</script>
六、最佳实践
6.1 何时使用哪种方式?
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| Vue 3.3+ 新项目 | defineModel | 代码简洁,易维护 |
| 需要兼容旧版本 | update:modelValue | 兼容性好 |
| 复杂验证逻辑 | update:modelValue | 更灵活 |
| 简单表单组件 | defineModel | 开发效率高 |
6.2 命名规范
vue
<!-- ✅ 推荐 -->
<CustomInput v-model="userName" />
<CustomInput v-model:email="userEmail" />
<!-- ❌ 不推荐 -->
<CustomInput v-model="user_name" />
<CustomInput v-model:Email="userEmail" />
6.3 性能优化
vue
<script setup>
// 使用 computed 缓存计算结果
const displayValue = computed(() => {
return formatValue(props.modelValue)
})
// 防抖处理频繁更新
import { useDebounceFn } from '@vueuse/core'
const debouncedEmit = useDebounceFn((value) => {
emit('update:modelValue', value)
}, 300)
</script>
七、总结
核心要点
- Vue 3 的 v-model 本质是
:modelValue+@update:modelValue的语法糖 - 手动实现 需要 defineProps + defineEmits,适合复杂场景
- defineModel 是 Vue 3.3+ 的新特性,大幅简化代码
- 不能直接修改 props,必须通过事件通知父组件
- 支持多个 v-model,适合复杂表单组件
快速对比
vue
<!-- 方式1:手动实现 -->
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
const handleInput = (e) => emit('update:modelValue', e.target.value)
</script>
<!-- 方式2:defineModel -->
<script setup>
const model = defineModel()
// 直接使用 model.value 读写
</script>
学习建议
- 先理解 v-model 的工作原理
- 掌握手动实现方式(理解底层)
- 在新项目中使用 defineModel(提高效率)
- 多写实际案例加深理解
参考资源
标签: Vue3, 双向绑定, v-model, defineModel, 组件通信
如果这篇文章对你有帮助,欢迎点赞、收藏、关注!有问题欢迎在评论区讨论。