Vue 双向数据绑定 (v-model)
核心概念
v-model 是 Vue 中实现双向数据绑定的语法糖,它会根据不同的表单元素自动选择对应的属性和事件。
v-model 原理
在表单元素上使用
v-model 本质上是以下两个操作的语法糖:
- 绑定 value 属性(或其他属性)
- 监听 input 事件(或其他事件)
vue
<!-- 使用 v-model -->
<input v-model="message" />
<!-- 等价于 -->
<input :value="message" @input="message = $event.target.value" />不同表单元素的实现
vue
<!-- 文本输入框 -->
<input v-model="text" />
<!-- 等价于 -->
<input :value="text" @input="text = $event.target.value" />
<!-- 复选框 -->
<input type="checkbox" v-model="checked" />
<!-- 等价于 -->
<input type="checkbox" :checked="checked" @change="checked = $event.target.checked" />
<!-- 单选框 -->
<input type="radio" value="option1" v-model="picked" />
<!-- 等价于 -->
<input type="radio" value="option1" :checked="picked === 'option1'" @change="picked = $event.target.value" />
<!-- 下拉框 -->
<select v-model="selected">
<option>选项A</option>
</select>
<!-- 等价于 -->
<select :value="selected" @change="selected = $event.target.value">
<option>选项A</option>
</select>修饰符
.lazy
默认情况下,v-model 在每次 input 事件触发后同步数据。使用 .lazy 修饰符可以改为在 change 事件后同步。
vue
<template>
<!-- 失去焦点时才更新 -->
<input v-model.lazy="message" />
</template>.number
自动将用户输入转换为数值类型。
vue
<template>
<input v-model.number="age" type="number" />
</template>
<script setup>
import { ref } from 'vue'
const age = ref(0)
// 输入 "123" 时,age 的值是数字 123,而不是字符串 "123"
</script>.trim
自动过滤用户输入的首尾空白字符。
vue
<template>
<input v-model.trim="username" />
</template>组合使用
vue
<template>
<input v-model.lazy.trim="message" />
<input v-model.number.lazy="age" />
</template>在组件上使用 v-model
Vue 3 中的用法
在 Vue 3 中,v-model 默认对应 modelValue prop 和 update:modelValue 事件。
vue
<!-- 父组件 -->
<template>
<CustomInput v-model="searchText" />
<!-- 等价于 -->
<CustomInput
:modelValue="searchText"
@update:modelValue="searchText = $event"
/>
</template>
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const searchText = ref('')
</script>vue
<!-- CustomInput.vue 子组件 -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps({
modelValue: String
})
defineEmits(['update:modelValue'])
</script>使用计算属性实现
更优雅的方式是使用计算属性:
vue
<!-- CustomInput.vue -->
<template>
<input v-model="value" />
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: String
})
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>多个 v-model 绑定
Vue 3 支持在同一个组件上使用多个 v-model:
vue
<!-- 父组件 -->
<template>
<UserProfile
v-model:name="userName"
v-model:age="userAge"
/>
</template>
<script setup>
import { ref } from 'vue'
import UserProfile from './UserProfile.vue'
const userName = ref('张三')
const userAge = ref(25)
</script>vue
<!-- UserProfile.vue 子组件 -->
<template>
<div>
<input v-model="nameValue" placeholder="姓名" />
<input v-model.number="ageValue" type="number" placeholder="年龄" />
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
name: String,
age: Number
})
const emit = defineEmits(['update:name', 'update:age'])
const nameValue = computed({
get: () => props.name,
set: (value) => emit('update:name', value)
})
const ageValue = computed({
get: () => props.age,
set: (value) => emit('update:age', value)
})
</script>自定义 v-model 修饰符
Vue 3 支持自定义 v-model 修饰符:
vue
<!-- 父组件 -->
<template>
<CustomInput v-model.capitalize="text" />
</template>
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const text = ref('')
</script>vue
<!-- CustomInput.vue -->
<template>
<input v-model="value" />
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: String,
modelModifiers: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
// 如果有 capitalize 修饰符,首字母大写
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
})
</script>Vue 2 中的用法
在 Vue 2 中,v-model 默认对应 value prop 和 input 事件。
vue
<!-- 父组件 -->
<template>
<CustomInput v-model="searchText" />
</template>
<script>
export default {
data() {
return {
searchText: ''
}
}
}
</script>vue
<!-- CustomInput.vue 子组件 -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
props: {
value: String
}
}
</script>自定义 prop 和 event (Vue 2)
可以通过 model 选项自定义:
vue
<!-- CustomCheckbox.vue -->
<template>
<input
type="checkbox"
:checked="checked"
@change="$emit('change', $event.target.checked)"
/>
</template>
<script>
export default {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
}
}
</script>vue
<!-- 使用 -->
<template>
<CustomCheckbox v-model="isChecked" />
<!-- 等价于 -->
<CustomCheckbox :checked="isChecked" @change="isChecked = $event" />
</template>实际应用场景
表单验证
vue
<template>
<form @submit.prevent="handleSubmit">
<div>
<label>用户名:</label>
<input v-model.trim="form.username" />
<span v-if="errors.username" class="error">{{ errors.username }}</span>
</div>
<div>
<label>年龄:</label>
<input v-model.number="form.age" type="number" />
<span v-if="errors.age" class="error">{{ errors.age }}</span>
</div>
<div>
<label>邮箱:</label>
<input v-model.lazy="form.email" type="email" />
<span v-if="errors.email" class="error">{{ errors.email }}</span>
</div>
<button type="submit">提交</button>
</form>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
const form = reactive({
username: '',
age: null,
email: ''
})
const errors = reactive({
username: '',
age: '',
email: ''
})
// 实时验证
watch(() => form.username, (value) => {
if (!value) {
errors.username = '用户名不能为空'
} else if (value.length < 3) {
errors.username = '用户名至少3个字符'
} else {
errors.username = ''
}
})
watch(() => form.age, (value) => {
if (!value) {
errors.age = '年龄不能为空'
} else if (value < 18) {
errors.age = '年龄必须大于18岁'
} else {
errors.age = ''
}
})
watch(() => form.email, (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!value) {
errors.email = '邮箱不能为空'
} else if (!emailRegex.test(value)) {
errors.email = '邮箱格式不正确'
} else {
errors.email = ''
}
})
const handleSubmit = () => {
if (!errors.username && !errors.age && !errors.email) {
console.log('表单提交:', form)
} else {
console.log('表单验证失败')
}
}
</script>
<style scoped>
.error {
color: red;
font-size: 12px;
margin-left: 10px;
}
form > div {
margin-bottom: 15px;
}
</style>搜索框组件
vue
<!-- SearchBox.vue -->
<template>
<div class="search-box">
<input
v-model="searchValue"
:placeholder="placeholder"
@keyup.enter="handleSearch"
class="search-input"
/>
<button @click="handleSearch" class="search-btn">搜索</button>
<button v-if="searchValue" @click="clear" class="clear-btn">清空</button>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: String,
placeholder: {
type: String,
default: '请输入搜索内容'
}
})
const emit = defineEmits(['update:modelValue', 'search'])
const searchValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const handleSearch = () => {
emit('search', searchValue.value)
}
const clear = () => {
searchValue.value = ''
emit('search', '')
}
</script>
<style scoped>
.search-box {
display: flex;
gap: 8px;
}
.search-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.search-btn,
.clear-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.search-btn {
background-color: #409eff;
color: white;
}
.clear-btn {
background-color: #f56c6c;
color: white;
}
</style>使用搜索框:
vue
<template>
<div>
<SearchBox
v-model="keyword"
@search="handleSearch"
placeholder="搜索文章..."
/>
<div v-if="keyword">当前搜索: {{ keyword }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import SearchBox from './SearchBox.vue'
const keyword = ref('')
const handleSearch = (value) => {
console.log('搜索:', value)
// 执行搜索逻辑
}
</script>常见问题
1. 为什么 v-model 不生效?
问题: 输入框的值不更新。
vue
<!-- 错误示例 -->
<script setup>
const message = 'hello' // 不是响应式的
</script>
<template>
<input v-model="message" />
</template>解决方案: 确保使用响应式数据。
vue
<script setup>
import { ref } from 'vue'
const message = ref('hello') // 响应式的
</script>
<template>
<input v-model="message" />
</template>2. 复选框数组绑定
vue
<template>
<div>
<input type="checkbox" v-model="checkedItems" value="apple" /> 苹果
<input type="checkbox" v-model="checkedItems" value="banana" /> 香蕉
<input type="checkbox" v-model="checkedItems" value="orange" /> 橙子
<p>选中的: {{ checkedItems }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const checkedItems = ref([])
// 输出: ['apple', 'banana'] 当苹果和香蕉被选中时
</script>3. 单选框组
vue
<template>
<div>
<input type="radio" v-model="picked" value="option1" /> 选项1
<input type="radio" v-model="picked" value="option2" /> 选项2
<input type="radio" v-model="picked" value="option3" /> 选项3
<p>选中的: {{ picked }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const picked = ref('option1') // 默认选中 option1
</script>4. 下拉框多选
vue
<template>
<div>
<select v-model="selected" multiple>
<option value="js">JavaScript</option>
<option value="ts">TypeScript</option>
<option value="vue">Vue</option>
<option value="react">React</option>
</select>
<p>选中的: {{ selected }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selected = ref([])
// 输出: ['js', 'vue'] 当选中 JavaScript 和 Vue 时
</script>性能优化
使用 .lazy 修饰符
对于不需要实时更新的输入框,使用 .lazy 可以减少触发次数。
vue
<template>
<!-- 每次输入都触发更新 -->
<input v-model="message1" />
<!-- 失去焦点时才触发更新 -->
<input v-model.lazy="message2" />
</template>防抖处理
对于需要实时搜索的场景,建议添加防抖:
vue
<template>
<input v-model="searchText" placeholder="搜索..." />
</template>
<script setup>
import { ref, watch } from 'vue'
import { debounce } from 'lodash-es'
const searchText = ref('')
const debouncedSearch = debounce((value) => {
console.log('执行搜索:', value)
// 调用搜索 API
}, 300)
watch(searchText, (newValue) => {
debouncedSearch(newValue)
})
</script>总结
- v-model 本质:语法糖,简化了属性绑定 + 事件监听
- 修饰符:.lazy、.number、.trim 提供便捷的数据处理
- 组件通信:Vue 3 使用 modelValue/update:modelValue,支持多个 v-model
- 自定义修饰符:可以实现更灵活的数据处理逻辑
- 性能优化:合理使用 .lazy、防抖等技术提升性能
面试重点
- 解释 v-model 的原理和实现机制
- 说明 Vue 2 和 Vue 3 中 v-model 的区别
- 如何在自定义组件中实现 v-model
- v-model 修饰符的作用和使用场景
- 如何实现多个 v-model 绑定
- 双向数据绑定的性能考虑