目录
- 一、核心概念
- [1.1 什么是双向绑定?](#1.1 什么是双向绑定?)
- [1.2 v-model 的本质](#1.2 v-model 的本质)
- [二、在表单元素上使用 v-model](#二、在表单元素上使用 v-model)
- [2.1 输入框 text / textarea](#2.1 输入框 text / textarea)
- [2.2 复选框 checkbox](#2.2 复选框 checkbox)
- [2.3 单选框 radio](#2.3 单选框 radio)
- [2.4 下拉框 select](#2.4 下拉框 select)
- [三、v-model 的修饰符](#三、v-model 的修饰符)
- [3.1 .lazy - 改为 change 事件触发](#3.1 .lazy - 改为 change 事件触发)
- [3.2 .number - 自动转为数字](#3.2 .number - 自动转为数字)
- [3.3 .trim - 自动去除首尾空格](#3.3 .trim - 自动去除首尾空格)
- [3.4 链式使用](#3.4 链式使用)
- [四、在自定义组件上使用 v-model](#四、在自定义组件上使用 v-model)
- [4.1 单个 v-model(默认)](#4.1 单个 v-model(默认))
- [4.2 多个 v-model(绑定不同属性)](#4.2 多个 v-model(绑定不同属性))
- [4.3 带类型的 TypeScript 版本](#4.3 带类型的 TypeScript 版本)
- [4.4 自定义 v-model 的名称和事件](#4.4 自定义 v-model 的名称和事件)
- [五、v-model 的高级用法](#五、v-model 的高级用法)
- [5.1 结合 computed 使用](#5.1 结合 computed 使用)
- [5.2 自定义组件的 v-model 修饰符](#5.2 自定义组件的 v-model 修饰符)
- [5.3 多个 v-model 的修饰符](#5.3 多个 v-model 的修饰符)
- 六、完整实战示例:地址表单组件
- 七、面试高频问题
- [Q1:v-model 的原理是什么?](#Q1:v-model 的原理是什么?)
- [Q2:v-model 和 .sync 修饰符的关系?](#Q2:v-model 和 .sync 修饰符的关系?)
- [Q3:如何在一个组件上使用多个 v-model?](#Q3:如何在一个组件上使用多个 v-model?)
- [Q4:v-model 和 ref 的区别?](#Q4:v-model 和 ref 的区别?)
- 八、快速记忆
一、核心概念
1.1 什么是双向绑定?
单向数据流: 数据变化 → 视图更新(但视图变化不会自动改数据)
双向绑定: 数据变化 → 视图更新 并且 视图变化 → 数据自动更新
单向: 数据 ——→ 视图
双向: 数据 ←→ 视图
1.2 v-model 的本质
html
<!-- v-model 写法 -->
<input v-model="username" />
<!-- 本质是语法糖,等价于: -->
<input :value="username" @input="username = $event.target.value" />
拆解:
-
:value="username"→ 把数据绑定到输入框的 value 属性(数据 → 视图) -
@input="username = $event.target.value"→ 输入时把新值赋给数据(视图 → 数据)
二、在表单元素上使用 v-model
2.1 输入框 text / textarea
html
<template>
<!-- 文本输入框 -->
<input v-model="message" />
<textarea v-model="description" />
<!-- 等价写法 -->
<input :value="message" @input="message = $event.target.value" />
</template>
<script setup>
import { ref } from 'vue'
const message = ref('')
const description = ref('')
</script>
2.2 复选框 checkbox
html
<template>
<!-- 单个复选框:绑定布尔值 -->
<input type="checkbox" v-model="isAgreed" />
<p>是否同意:{{ isAgreed }}</p>
<!-- 多个复选框:绑定数组 -->
<input type="checkbox" value="苹果" v-model="fruits" />
<input type="checkbox" value="香蕉" v-model="fruits" />
<input type="checkbox" value="橙子" v-model="fruits" />
<p>选中的水果:{{ fruits }}</p>
</template>
<script setup>
import { ref } from 'vue'
const isAgreed = ref(false)
const fruits = ref([])
</script>
2.3 单选框 radio
html
<template>
<input type="radio" value="男" v-model="gender" />
<input type="radio" value="女" v-model="gender" />
<p>性别:{{ gender }}</p>
</template>
<script setup>
import { ref } from 'vue'
const gender = ref('')
</script>
2.4 下拉框 select
html
<template>
<!-- 单选 -->
<select v-model="city">
<option value="北京">北京</option>
<option value="上海">上海</option>
<option value="广州">广州</option>
</select>
<!-- 多选(加 multiple 属性) -->
<select v-model="cities" multiple>
<option value="北京">北京</option>
<option value="上海">上海</option>
<option value="广州">广州</option>
</select>
</template>
<script setup>
import { ref } from 'vue'
const city = ref('')
const cities = ref([])
</script>
三、v-model 的修饰符
3.1 .lazy - 改为 change 事件触发
html
<!-- 默认:input 事件(实时触发) -->
<input v-model="username" />
<!-- .lazy:改为 change 事件(失焦或回车时触发) -->
<input v-model.lazy="username" />
3.2 .number - 自动转为数字
html
<!-- 即使输入"123",也会自动转为数字 123 -->
<input v-model.number="age" type="number" />
3.3 .trim - 自动去除首尾空格
html
<!-- 输入" 张三 " 会变成 "张三" -->
<input v-model.trim="username" />
3.4 链式使用
html
<!-- 可以组合使用 -->
<input v-model.trim.lazy="username" />
<input v-model.number="age" />
四、在自定义组件上使用 v-model
4.1 单个 v-model(默认)
父组件:
html
<template>
<!-- 使用 v-model -->
<CustomInput v-model="username" />
<!-- 本质是: -->
<CustomInput
:modelValue="username"
@update:modelValue="username = $event"
/>
</template>
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const username = ref('张三')
</script>
子组件 CustomInput.vue:
html
<template>
<div>
<!-- 接收 props.value,触发 update:modelValue 事件 -->
<input
type="text"
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</div>
</template>
<script setup>
// 接收默认的 modelValue
defineProps(['modelValue'])
// 声明 update:modelValue 事件
const emit = defineEmits(['update:modelValue'])
</script>
4.2 多个 v-model(绑定不同属性)
父组件:
html
<template>
<!-- 多个 v-model,指定不同的绑定名 -->
<UserForm
v-model:firstName="firstName"
v-model:lastName="lastName"
v-model:age="age"
/>
<!-- 本质是: -->
<UserForm
:firstName="firstName"
:lastName="lastName"
:age="age"
@update:firstName="firstName = $event"
@update:lastName="lastName = $event"
@update:age="age = $event"
/>
</template>
<script setup>
import { ref } from 'vue'
import UserForm from './UserForm.vue'
const firstName = ref('张')
const lastName = ref('三')
const age = ref(18)
</script>
子组件 UserForm.vue:
html
<template>
<div>
<input
type="text"
:value="firstName"
@input="emit('update:firstName', $event.target.value)"
placeholder="姓"
/>
<input
type="text"
:value="lastName"
@input="emit('update:lastName', $event.target.value)"
placeholder="名"
/>
<input
type="number"
:value="age"
@input="emit('update:age', parseInt($event.target.value))"
placeholder="年龄"
/>
</div>
</template>
<script setup>
// 接收多个 props
defineProps(['firstName', 'lastName', 'age'])
// 声明多个更新事件
const emit = defineEmits(['update:firstName', 'update:lastName', 'update:age'])
</script>
4.3 带类型的 TypeScript 版本
html
<script setup lang="ts">
// 定义 props 类型
interface Props {
firstName: string
lastName: string
age: number
}
defineProps<Props>()
// 定义 emit 类型
const emit = defineEmits<{
'update:firstName': [value: string]
'update:lastName': [value: string]
'update:age': [value: number]
}>()
</script>
4.4 自定义 v-model 的名称和事件
html
<!-- 父组件:可以自定义绑定的属性名 -->
<CustomInput v-model:title="bookTitle" />
<!-- 子组件需要对应接收 -->
<script setup>
defineProps(['title'])
const emit = defineEmits(['update:title'])
</script>
五、v-model 的高级用法
5.1 结合 computed 使用
html
<template>
<!-- 在子组件内部使用 computed 包装 v-model -->
<input v-model="localValue" />
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
// 创建一个可读可写的 computed
const localValue = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
5.2 自定义组件的 v-model 修饰符
html
<!-- 父组件:使用自定义修饰符 -->
<CustomInput v-model.capitalize="username" />
<!-- 子组件:接收修饰符 -->
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { // 接收修饰符
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue'])
function onInput(e) {
let value = e.target.value
// 如果有 capitalize 修饰符,首字母大写
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
5.3 多个 v-model 的修饰符
html
<!-- 父组件 -->
<ChildComponent
v-model:title.capitalize="pageTitle"
v-model:content.uppercase="pageContent"
/>
<!-- 子组件 -->
<script setup>
const props = defineProps({
title: String,
titleModifiers: { type: Object, default: () => ({}) },
content: String,
contentModifiers: { type: Object, default: () => ({}) }
})
const emit = defineEmits(['update:title', 'update:content'])
function onTitleInput(e) {
let value = e.target.value
if (props.titleModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:title', value)
}
function onContentInput(e) {
let value = e.target.value
if (props.contentModifiers.uppercase) {
value = value.toUpperCase()
}
emit('update:content', value)
}
</script>
六、完整实战示例:地址表单组件
html
<!-- 父组件 App.vue -->
<template>
<div>
<h2>地址信息</h2>
<p>省份:{{ address.province }}</p>
<p>城市:{{ address.city }}</p>
<p>区县:{{ address.district }}</p>
<p>详细地址:{{ address.detail }}</p>
<AddressForm
v-model:province="address.province"
v-model:city="address.city"
v-model:district="address.district"
v-model:detail="address.detail"
/>
</div>
</template>
<script setup>
import { reactive } from 'vue'
import AddressForm from './AddressForm.vue'
const address = reactive({
province: '',
city: '',
district: '',
detail: ''
})
</script>
html
<!-- 子组件 AddressForm.vue -->
<template>
<div class="address-form">
<div class="form-group">
<label>省份</label>
<select :value="province" @change="emit('update:province', $event.target.value)">
<option value="">请选择</option>
<option value="广东">广东</option>
<option value="北京">北京</option>
<option value="上海">上海</option>
</select>
</div>
<div class="form-group">
<label>城市</label>
<input
type="text"
:value="city"
@input="emit('update:city', $event.target.value)"
placeholder="请输入城市"
/>
</div>
<div class="form-group">
<label>区县</label>
<input
type="text"
:value="district"
@input="emit('update:district', $event.target.value)"
placeholder="请输入区县"
/>
</div>
<div class="form-group">
<label>详细地址</label>
<textarea
:value="detail"
@input="emit('update:detail', $event.target.value)"
rows="3"
placeholder="请输入详细地址"
></textarea>
</div>
</div>
</template>
<script setup>
defineProps(['province', 'city', 'district', 'detail'])
const emit = defineEmits([
'update:province',
'update:city',
'update:district',
'update:detail'
])
</script>
<style scoped>
.address-form {
border: 1px solid #ccc;
padding: 20px;
border-radius: 8px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, select, textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
七、面试高频问题
Q1:v-model 的原理是什么?
v-model 是语法糖,本质是 props + emit 。在表单元素上,它绑定
value属性并监听input事件;在自定义组件上,默认绑定modelValueprop 并监听update:modelValue事件。
Q2:v-model 和 .sync 修饰符的关系?
Vue 2 中有
.sync修饰符用于双向绑定,Vue 3 中.sync被废弃,统一用 v-model 并支持多个绑定。
vue
<!-- Vue 2 写法 -->
<Child :title.sync="pageTitle" />
<!-- Vue 3 写法(等价) -->
<Child v-model:title="pageTitle" />
Q3:如何在一个组件上使用多个 v-model?
通过指定不同的绑定名:
v-model:propName="data",子组件接收对应的 prop 并触发update:propName事件。
Q4:v-model 和 ref 的区别?
| 对比 | v-model | ref |
|---|---|---|
| 绑定方式 | 双向绑定 | 单向引用 |
| 数据流向 | 数据 ↔ 视图 | 只获取 DOM 或组件实例 |
| 使用场景 | 表单输入、组件通信 | 获取 DOM 元素、调用子组件方法 |
八、快速记忆
v-model 是语法糖
props + emit 是本质
表单元素上:
:value + @input
自定义组件上:
:modelValue + @update:modelValue
多个绑定用冒号:
v-model:firstName + v-model:lastName
修饰符三兄弟:
.lazy(失焦触发)
.number(转数字)
.trim(去空格)