Vue3.0: v-model 组件双向绑定学习文档 (v3.4 前后对比 + TypeScript)
·
Vue3.0: v-model 组件双向绑定学习文档 (v3.4 前后对比 + TypeScript)
文章目录
1. v-model 基础概念
1.1 什么是 v-model
v-model 是 Vue.js 中用于实现表单输入元素双向数据绑定的指令,是 :modelValue 和 @update:modelValue 的语法糖。
2. Vue 3.4 之前的 v-model 用法 (TypeScript)
2.1 基本用法
子组件实现:
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
>
</template>
<script setup lang="ts">
interface Props {
modelValue: string
}
interface Emits {
(e: 'update:modelValue', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
</script>
父组件使用:
<template>
<div>
<CustomInput v-model="inputValue" />
<p>输入的值: {{ inputValue }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const inputValue = ref<string>('')
</script>
2.2 多个 v-model 绑定
子组件:
<template>
<div class="user-form">
<input
:value="firstName"
@input="$emit('update:firstName', ($event.target as HTMLInputElement).value)"
placeholder="名"
>
<input
:value="lastName"
@input="$emit('update:lastName', ($event.target as HTMLInputElement).value)"
placeholder="姓"
>
</div>
</template>
<script setup lang="ts">
interface Props {
firstName: string
lastName: string
}
interface Emits {
(e: 'update:firstName', value: string): void
(e: 'update:lastName', value: string): void
}
defineProps<Props>()
defineEmits<Emits>()
</script>
父组件:
<template>
<div>
<UserForm
v-model:firstName="firstName"
v-model:lastName="lastName"
/>
<p>全名: {{ firstName }} {{ lastName }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import UserForm from './UserForm.vue'
const firstName = ref<string>('')
const lastName = ref<string>('')
</script>
2.3 带类型的模型
子组件:
<template>
<div class="number-input">
<button @click="decrement">-</button>
<input
:value="modelValue"
@input="handleInput"
type="number"
>
<button @click="increment">+</button>
</div>
</template>
<script setup lang="ts">
interface Props {
modelValue: number
min?: number
max?: number
}
interface Emits {
(e: 'update:modelValue', value: number): void
}
const props = withDefaults(defineProps<Props>(), {
min: 0,
max: 100
})
const emit = defineEmits<Emits>()
const handleInput = (event: Event) => {
const value = Number((event.target as HTMLInputElement).value)
emit('update:modelValue', Math.min(Math.max(value, props.min), props.max))
}
const increment = () => {
const newValue = Math.min(props.modelValue + 1, props.max)
emit('update:modelValue', newValue)
}
const decrement = () => {
const newValue = Math.max(props.modelValue - 1, props.min)
emit('update:modelValue', newValue)
}
</script>
2.4 修饰符处理
子组件:
<template>
<input
:value="modelValue"
@input="handleInput"
:class="{ 'capitalized': hasCapitalizeModifier }"
>
</template>
<script setup lang="ts">
interface Props {
modelValue: string
modelModifiers?: {
capitalize?: boolean
trim?: boolean
}
}
interface Emits {
(e: 'update:modelValue', value: string): void
}
const props = withDefaults(defineProps<Props>(), {
modelModifiers: () => ({})
})
const emit = defineEmits<Emits>()
const hasCapitalizeModifier = props.modelModifiers.capitalize
const handleInput = (event: Event) => {
let value = (event.target as HTMLInputElement).value
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
if (props.modelModifiers.trim) {
value = value.trim()
}
emit('update:modelValue', value)
}
</script>
父组件使用修饰符:
<template>
<div>
<CustomInput v-model.capitalize.trim="textValue" />
<p>处理后的文本: "{{ textValue }}"</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const textValue = ref<string>('')
</script>
3. Vue 3.4 之后的新 v-model 用法 (TypeScript)
3.1 defineModel() 宏的引入
基本用法:
<template>
<input v-model="model">
</template>
<script setup lang="ts">
// 自动推断为 Ref<string>
const model = defineModel<string>()
</script>
3.2 新旧写法对比
| 特性 | Vue 3.4 之前 | Vue 3.4 之后 |
|---|---|---|
| 声明 | defineProps() + defineEmits() |
defineModel() |
| 类型定义 | 需要定义 Props 和 Emits 接口 | 自动类型推断或简单泛型 |
| 代码量 | 较多样板代码 | 极简 |
3.3 详细用法对比
3.3.1 基本输入框组件
Vue 3.4 之前:
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
:placeholder="placeholder"
>
</template>
<script setup lang="ts">
interface Props {
modelValue: string
placeholder?: string
}
interface Emits {
(e: 'update:modelValue', value: string): void
}
defineProps<Props>()
defineEmits<Emits>()
</script>
Vue 3.4 之后:
<template>
<input
v-model="model"
:placeholder="placeholder"
>
</template>
<script setup lang="ts">
interface Props {
placeholder?: string
}
const model = defineModel<string>('modelValue', { required: true })
defineProps<Props>()
</script>
3.3.2 带类型的模型
Vue 3.4 之前:
<template>
<input
:value="modelValue"
@input="handleInput"
type="number"
>
</template>
<script setup lang="ts">
interface Props {
modelValue: number
}
interface Emits {
(e: 'update:modelValue', value: number): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const handleInput = (event: Event) => {
const value = Number((event.target as HTMLInputElement).value)
emit('update:modelValue', value)
}
</script>
Vue 3.4 之后:
<template>
<input
v-model="model"
type="number"
>
</template>
<script setup lang="ts">
// 自动处理类型转换,model 的类型是 Ref<number>
const model = defineModel<number>()
</script>
3.4 多个 v-model 绑定
Vue 3.4 之前:
<template>
<div class="user-form">
<input
:value="firstName"
@input="$emit('update:firstName', ($event.target as HTMLInputElement).value)"
placeholder="名"
>
<input
:value="lastName"
@input="$emit('update:lastName', ($event.target as HTMLInputElement).value)"
placeholder="姓"
>
</div>
</template>
<script setup lang="ts">
interface Props {
firstName: string
lastName: string
}
interface Emits {
(e: 'update:firstName', value: string): void
(e: 'update:lastName', value: string): void
}
defineProps<Props>()
defineEmits<Emits>()
</script>
Vue 3.4 之后:
<template>
<div class="user-form">
<input v-model="firstNameModel" placeholder="名">
<input v-model="lastNameModel" placeholder="姓">
</div>
</template>
<script setup lang="ts">
const firstNameModel = defineModel<string>('firstName')
const lastNameModel = defineModel<string>('lastName')
</script>
3.5 修饰符处理
Vue 3.4 之前:
<template>
<input
:value="modelValue"
@input="handleInput"
>
</template>
<script setup lang="ts">
interface Props {
modelValue: string
modelModifiers?: {
capitalize?: boolean
trim?: boolean
}
}
interface Emits {
(e: 'update:modelValue', value: string): void
}
const props = withDefaults(defineProps<Props>(), {
modelModifiers: () => ({})
})
const emit = defineEmits<Emits>()
const handleInput = (event: Event) => {
let value = (event.target as HTMLInputElement).value
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
if (props.modelModifiers.trim) {
value = value.trim()
}
emit('update:modelValue', value)
}
</script>
Vue 3.4 之后:
<template>
<input v-model="modelValue">
</template>
<script setup lang="ts">
const modelValue = defineModel<string>({
set(value) {
if (modelValue.modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
if (modelValue.modifiers.trim) {
return value.trim()
}
return value
}
})
</script>
3.6 高级用法
3.6.1 带默认值和验证
<template>
<input v-model="model">
</template>
<script setup lang="ts">
interface User {
name: string
age: number
}
const model = defineModel<User>({
required: true,
default: () => ({ name: '匿名', age: 18 }),
validator(value: User): boolean {
return value.name.length > 0 && value.age >= 0
}
})
</script>
3.6.2 自定义 getter/setter
<template>
<input v-model="displayValue">
</template>
<script setup lang="ts">
import { computed } from 'vue'
const model = defineModel<string>()
const displayValue = computed({
get: () => model.value ? model.value.toUpperCase() : '',
set: (value: string) => {
model.value = value.toLowerCase()
}
})
</script>
3.6.3 复杂组件示例
Vue 3.4 之前:
<template>
<div class="range-slider">
<input
type="range"
:min="min"
:max="max"
:value="modelValue"
@input="$emit('update:modelValue', Number(($event.target as HTMLInputElement).value))"
>
<span>{{ modelValue }}</span>
</div>
</template>
<script setup lang="ts">
interface Props {
modelValue: number
min?: number
max?: number
}
interface Emits {
(e: 'update:modelValue', value: number): void
}
const props = withDefaults(defineProps<Props>(), {
min: 0,
max: 100
})
defineEmits<Emits>()
</script>
Vue 3.4 之后:
<template>
<div class="range-slider">
<input
type="range"
:min="min"
:max="max"
v-model="model"
>
<span>{{ model }}</span>
</div>
</template>
<script setup lang="ts">
interface Props {
min?: number
max?: number
}
const model = defineModel<number>({ required: true })
const props = withDefaults(defineProps<Props>(), {
min: 0,
max: 100
})
</script>
4. 实际项目示例
4.1 搜索组件迁移
Vue 3.4 之前:
<template>
<div class="search-box">
<input
:value="modelValue"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
placeholder="搜索..."
/>
<button @click="$emit('search', modelValue)">搜索</button>
</div>
</template>
<script setup lang="ts">
interface Props {
modelValue: string
}
interface Emits {
(e: 'update:modelValue', value: string): void
(e: 'search', value: string): void
}
defineProps<Props>()
defineEmits<Emits>()
</script>
Vue 3.4 之后:
<template>
<div class="search-box">
<input v-model="searchText" placeholder="搜索..." />
<button @click="emit('search', searchText)">搜索</button>
</div>
</template>
<script setup lang="ts">
interface Emits {
(e: 'search', value: string): void
}
const searchText = defineModel<string>('modelValue', { required: true })
const emit = defineEmits<Emits>()
</script>
4.2 表单选择器组件
Vue 3.4 之后:
<template>
<div class="select-wrapper">
<select v-model="selectedValue">
<option
v-for="option in options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
</template>
<script setup lang="ts">
interface Option {
value: string | number
label: string
}
interface Props {
options: Option[]
}
const selectedValue = defineModel<string | number>('modelValue', { required: true })
defineProps<Props>()
</script>
5. 类型安全和最佳实践
5.1 严格的类型定义
<template>
<input v-model="emailModel">
</template>
<script setup lang="ts">
// 使用联合类型
type Status = 'active' | 'inactive' | 'pending'
const statusModel = defineModel<Status>()
// 使用接口
interface UserSettings {
theme: 'light' | 'dark'
notifications: boolean
language: string
}
const settingsModel = defineModel<UserSettings>({
default: () => ({
theme: 'light',
notifications: true,
language: 'zh-CN'
})
})
// 电子邮件验证
const emailModel = defineModel<string>({
set(value: string) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value) && value !== '') {
console.warn('无效的电子邮件格式')
}
return value
}
})
</script>
5.2 异步操作处理
<template>
<div class="async-input">
<input
v-model="localValue"
@blur="saveChanges"
:disabled="isSaving"
>
<span v-if="isSaving">保存中...</span>
</div>
</template>
<script setup lang="ts>
import { ref, watch } from 'vue'
const model = defineModel<string>()
const isSaving = ref(false)
const localValue = ref(model.value)
// 监听外部变化
watch(model, (newValue) => {
localValue.value = newValue
})
const saveChanges = async () => {
if (localValue.value === model.value) return
isSaving.value = true
try {
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1000))
model.value = localValue.value
} catch (error) {
console.error('保存失败:', error)
} finally {
isSaving.value = false
}
}
</script>
6. 总结
Vue 3.4 的 defineModel() 结合 TypeScript 带来了显著的改进:
主要优势:
- 极简代码:大幅减少类型定义和样板代码
- 完美类型推断:自动的类型安全和智能提示
- 更好的开发体验:代码更简洁,维护更轻松
- 编译时优化:更好的性能表现
TypeScript 最佳实践:
- 始终为
defineModel()提供泛型类型参数 - 利用 TypeScript 的联合类型和接口确保类型安全
- 在复杂场景中使用自定义 setter 进行数据验证
- 合理使用默认值和验证器
更多推荐
所有评论(0)