Vue3.0: v-model 组件双向绑定学习文档 (v3.4 前后对比 + TypeScript)
Vue3.0: v-model 组件双向绑定学习文档 (v3.4 前后对比 + TypeScript)
文章目录
- Vue3.0: v-model 组件双向绑定学习文档 (v3.4 前后对比 + TypeScript)
- 1. v-model 基础概念
- 1.1 什么是 v-model
- 2. Vue 3.4 之前的 v-model 用法 (TypeScript)
- 2.1 基本用法
- 2.2 多个 v-model 绑定
- 2.3 带类型的模型
- 2.4 修饰符处理
- 3. Vue 3.4 之后的新 v-model 用法 (TypeScript)
- 3.1 `defineModel()` 宏的引入
- 3.2 新旧写法对比
- 3.3 详细用法对比
- 3.3.1 基本输入框组件
- 3.3.2 带类型的模型
- 3.4 多个 v-model 绑定
- 3.5 修饰符处理
- 3.6 高级用法
- 3.6.1 带默认值和验证
- 3.6.2 自定义 getter/setter
- 3.6.3 复杂组件示例
- 4. 实际项目示例
- 4.1 搜索组件迁移
- 4.2 表单选择器组件
- 5. 类型安全和最佳实践
- 5.1 严格的类型定义
- 5.2 异步操作处理
- 6. 总结
- 主要优势:
- 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: stringlastName: string
}interface Emits {(e: 'update:firstName', value: string): void(e: 'update:lastName', value: string): void
}defineProps<Props>()
defineEmits<Emits>()
</script>
父组件:
<template><div><UserFormv-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: numbermin?: numbermax?: 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: stringmodelModifiers?: {capitalize?: booleantrim?: boolean}
}interface Emits {(e: 'update:modelValue', value: string): void
}const props = withDefaults(defineProps<Props>(), {modelModifiers: () => ({})
})const emit = defineEmits<Emits>()const hasCapitalizeModifier = props.modelModifiers.capitalizeconst handleInput = (event: Event) => {let value = (event.target as HTMLInputElement).valueif (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: stringplaceholder?: string
}interface Emits {(e: 'update:modelValue', value: string): void
}defineProps<Props>()
defineEmits<Emits>()
</script>
Vue 3.4 之后:
<template><inputv-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><inputv-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: stringlastName: 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: stringmodelModifiers?: {capitalize?: booleantrim?: 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).valueif (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: stringage: 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"><inputtype="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: numbermin?: numbermax?: 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"><inputtype="range":min="min":max="max"v-model="model"><span>{{ model }}</span></div>
</template><script setup lang="ts">
interface Props {min?: numbermax?: 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"><optionv-for="option in options":key="option.value":value="option.value">{{ option.label }}</option></select></div>
</template><script setup lang="ts">
interface Option {value: string | numberlabel: 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: booleanlanguage: 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"><inputv-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) returnisSaving.value = truetry {// 模拟 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 进行数据验证
- 合理使用默认值和验证器