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 最佳实践:

  1. 始终为 defineModel() 提供泛型类型参数
  2. 利用 TypeScript 的联合类型和接口确保类型安全
  3. 在复杂场景中使用自定义 setter 进行数据验证
  4. 合理使用默认值和验证器
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐