Vue 3 组件双向绑定完全指南:update:modelValue 与 defineModel

本文详细介绍 Vue 3 中实现组件双向绑定的两种方式,帮助你深入理解 v-model 的工作原理。

一、什么是 v-model?

v-model 是 Vue 提供的语法糖,用于实现父子组件之间的双向数据绑定。在 Vue 3 中,v-model 的底层实现发生了变化。

Vue 2 vs Vue 3

Vue 2:

vue 复制代码
<!-- 父组件 -->
<CustomInput v-model="message" />

<!-- 等价于 -->
<CustomInput 
  :value="message" 
  @input="message = $event" 
/>

Vue 3:

vue 复制代码
<!-- 父组件 -->
<CustomInput v-model="message" />

<!-- 等价于 -->
<CustomInput 
  :modelValue="message" 
  @update:modelValue="message = $event" 
/>

关键变化:

  • prop 名从 value 改为 modelValue
  • 事件名从 input 改为 update:modelValue

二、方法一:手动实现(update:modelValue)

这是传统方式,需要手动定义 props 和 emits。

2.1 基础示例

子组件 (CustomInput.vue):

vue 复制代码
<template>
  <div class="custom-input">
    <input 
      :value="modelValue"
      @input="handleInput"
      placeholder="请输入内容"
    />
  </div>
</template>

<script setup>
// 1. 定义 props,接收父组件传入的值
const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  }
})

// 2. 定义 emits,声明要触发的事件
const emit = defineEmits(['update:modelValue'])

// 3. 处理输入事件,触发更新
const handleInput = (event) => {
  emit('update:modelValue', event.target.value)
}
</script>

父组件:

vue 复制代码
<template>
  <div>
    <CustomInput v-model="message" />
    <p>输入的内容:{{ message }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const message = ref('Hello')
</script>

2.2 工作原理

  1. 父组件传值 :通过 :modelValue="message" 将数据传给子组件
  2. 子组件接收 :通过 props.modelValue 接收数据
  3. 子组件更新 :通过 emit('update:modelValue', newValue) 通知父组件
  4. 父组件响应 :自动执行 message = newValue

2.3 完整示例:自定义选择器

vue 复制代码
<template>
  <div class="custom-select">
    <div 
      v-for="option in options" 
      :key="option.value"
      class="option"
      :class="{ active: modelValue === option.value }"
      @click="handleSelect(option.value)"
    >
      {{ option.label }}
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  modelValue: {
    type: [String, Number],
    default: null
  },
  options: {
    type: Array,
    default: () => []
  }
})

const emit = defineEmits(['update:modelValue', 'change'])

const handleSelect = (value) => {
  // 触发 v-model 更新
  emit('update:modelValue', value)
  // 触发额外的 change 事件
  emit('change', value)
}
</script>

<style scoped>
.option {
  padding: 8px 16px;
  cursor: pointer;
  border: 1px solid #ddd;
}

.option.active {
  background: #409eff;
  color: white;
}
</style>

使用方式:

vue 复制代码
<template>
  <CustomSelect 
    v-model="selectedValue"
    :options="options"
    @change="handleChange"
  />
</template>

<script setup>
import { ref } from 'vue'

const selectedValue = ref('option1')
const options = [
  { label: '选项1', value: 'option1' },
  { label: '选项2', value: 'option2' },
  { label: '选项3', value: 'option3' }
]

const handleChange = (value) => {
  console.log('选中了:', value)
}
</script>

三、方法二:defineModel(Vue 3.3+)

Vue 3.3 引入的新特性,大幅简化双向绑定的实现。

3.1 基础用法

子组件 (CustomInput.vue):

vue 复制代码
<template>
  <div class="custom-input">
    <input 
      v-model="model"
      placeholder="请输入内容"
    />
  </div>
</template>

<script setup>
// 一行代码搞定!
const model = defineModel()
</script>

父组件:

vue 复制代码
<template>
  <CustomInput v-model="message" />
</template>

<script setup>
import { ref } from 'vue'

const message = ref('Hello')
</script>

3.2 defineModel 的优势

特性 手动实现 defineModel
代码量 需要 props + emit 一行代码
类型定义 手动定义 自动推导
可读性 较复杂 简洁明了
直接修改 不可以 可以

3.3 带选项的 defineModel

vue 复制代码
<template>
  <input 
    v-model="model"
    type="number"
  />
</template>

<script setup>
// 定义类型、默认值、验证器
const model = defineModel({
  type: Number,
  default: 0,
  required: false,
  validator: (value) => value >= 0
})

// 可以直接修改
const increment = () => {
  model.value++
}
</script>

3.4 多个 v-model

Vue 3 支持多个 v-model,非常适合复杂表单组件。

子组件 (UserForm.vue):

vue 复制代码
<template>
  <div class="user-form">
    <input v-model="name" placeholder="姓名" />
    <input v-model="email" placeholder="邮箱" />
    <input v-model="age" type="number" placeholder="年龄" />
  </div>
</template>

<script setup>
// 定义多个 model
const name = defineModel('name', { type: String, default: '' })
const email = defineModel('email', { type: String, default: '' })
const age = defineModel('age', { type: Number, default: 0 })
</script>

父组件:

vue 复制代码
<template>
  <UserForm 
    v-model:name="userName"
    v-model:email="userEmail"
    v-model:age="userAge"
  />
  
  <div>
    <p>姓名: {{ userName }}</p>
    <p>邮箱: {{ userEmail }}</p>
    <p>年龄: {{ userAge }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const userName = ref('张三')
const userEmail = ref('zhangsan@example.com')
const userAge = ref(25)
</script>

四、实战案例:类型选择器

基于你的项目代码,这是一个实际的应用示例。

4.1 使用 update:modelValue 实现

vue 复制代码
<template>
  <div class="dict-select">
    <span v-if="label" class="filter-label">{{ label }}</span>
    <div class="filter-options">
      <span 
        v-for="item in dictOptions" 
        :key="item.value"
        class="filter-item"
        :class="{ 'active': String(modelValue) === String(item.value) }"
        @click="handleSelect(item.value)"
      >
        {{ item.label }}
      </span>
      <span 
        class="filter-item" 
        :class="{ 'active': modelValue === null }"
        @click="handleSelect(null)"
      >
        全部
      </span>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  modelValue: {
    type: [String, Number],
    default: null
  },
  dictData: {
    type: Array,
    default: () => []
  },
  label: {
    type: String,
    default: ''
  }
})

const emit = defineEmits(['update:modelValue', 'change'])

const dictOptions = computed(() => {
  return props.dictData || []
})

const handleSelect = (value) => {
  emit('update:modelValue', value)
  emit('change', value)
}
</script>

使用:

vue 复制代码
<template>
  <DictSelect 
    v-model="queryParams.invoiceType"
    :dictData="finance_invoice_category"
    label="类型"
    @change="handleInvoiceTypeChange"
  />
</template>

<script setup>
const queryParams = ref({
  invoiceType: null
})

const handleInvoiceTypeChange = (value) => {
  console.log('类型改变:', value)
  // 重新加载数据
  loadData()
}
</script>

4.2 使用 defineModel 重构

vue 复制代码
<template>
  <div class="dict-select">
    <span v-if="label" class="filter-label">{{ label }}</span>
    <div class="filter-options">
      <span 
        v-for="item in dictData" 
        :key="item.value"
        class="filter-item"
        :class="{ 'active': String(model) === String(item.value) }"
        @click="handleSelect(item.value)"
      >
        {{ item.label }}
      </span>
      <span 
        class="filter-item" 
        :class="{ 'active': model === null }"
        @click="handleSelect(null)"
      >
        全部
      </span>
    </div>
  </div>
</template>

<script setup>
// 使用 defineModel 简化代码
const model = defineModel({
  type: [String, Number],
  default: null
})

defineProps({
  dictData: {
    type: Array,
    default: () => []
  },
  label: {
    type: String,
    default: ''
  }
})

const emit = defineEmits(['change'])

const handleSelect = (value) => {
  model.value = value  // 直接修改,自动触发更新
  emit('change', value)
}
</script>

五、常见问题与解决方案

5.1 为什么不能直接修改 props?

vue 复制代码
<!-- ❌ 错误做法 -->
<script setup>
const props = defineProps({
  modelValue: String
})

// 这样会报错!
const handleInput = (e) => {
  props.modelValue = e.target.value  // ❌ 不允许
}
</script>

<!-- ✅ 正确做法 -->
<script setup>
const props = defineProps({
  modelValue: String
})

const emit = defineEmits(['update:modelValue'])

const handleInput = (e) => {
  emit('update:modelValue', e.target.value)  // ✅ 通过事件通知父组件
}
</script>

原因: Vue 遵循单向数据流原则,子组件不能直接修改父组件的数据。

5.2 如何在子组件中使用 v-model?

vue 复制代码
<template>
  <!-- ❌ 错误:不能直接对 prop 使用 v-model -->
  <input v-model="modelValue" />
</template>

<script setup>
const props = defineProps({
  modelValue: String
})
</script>

解决方案 1:使用 :value 和 @input

vue 复制代码
<template>
  <input 
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

解决方案 2:使用 computed

vue 复制代码
<template>
  <input v-model="localValue" />
</template>

<script setup>
const props = defineProps({
  modelValue: String
})

const emit = defineEmits(['update:modelValue'])

const localValue = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
})
</script>

解决方案 3:使用 defineModel(推荐)

vue 复制代码
<template>
  <input v-model="model" />
</template>

<script setup>
const model = defineModel()
</script>

5.3 类型转换问题

vue 复制代码
<script setup>
const props = defineProps({
  modelValue: {
    type: Number,  // 期望是数字
    default: 0
  }
})

const emit = defineEmits(['update:modelValue'])

const handleInput = (e) => {
  // input 的 value 是字符串,需要转换
  const value = parseFloat(e.target.value) || 0
  emit('update:modelValue', value)
}
</script>

5.4 对象和数组的双向绑定

vue 复制代码
<template>
  <div>
    <input v-model="localForm.name" />
    <input v-model="localForm.age" type="number" />
  </div>
</template>

<script setup>
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => ({})
  }
})

const emit = defineEmits(['update:modelValue'])

// 使用 computed 处理对象
const localForm = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
})

// 或者使用 watch 深度监听
const localForm = ref({ ...props.modelValue })

watch(localForm, (newVal) => {
  emit('update:modelValue', newVal)
}, { deep: true })
</script>

六、最佳实践

6.1 何时使用哪种方式?

场景 推荐方式 原因
Vue 3.3+ 新项目 defineModel 代码简洁,易维护
需要兼容旧版本 update:modelValue 兼容性好
复杂验证逻辑 update:modelValue 更灵活
简单表单组件 defineModel 开发效率高

6.2 命名规范

vue 复制代码
<!-- ✅ 推荐 -->
<CustomInput v-model="userName" />
<CustomInput v-model:email="userEmail" />

<!-- ❌ 不推荐 -->
<CustomInput v-model="user_name" />
<CustomInput v-model:Email="userEmail" />

6.3 性能优化

vue 复制代码
<script setup>
// 使用 computed 缓存计算结果
const displayValue = computed(() => {
  return formatValue(props.modelValue)
})

// 防抖处理频繁更新
import { useDebounceFn } from '@vueuse/core'

const debouncedEmit = useDebounceFn((value) => {
  emit('update:modelValue', value)
}, 300)
</script>

七、总结

核心要点

  1. Vue 3 的 v-model 本质是 :modelValue + @update:modelValue 的语法糖
  2. 手动实现 需要 defineProps + defineEmits,适合复杂场景
  3. defineModel 是 Vue 3.3+ 的新特性,大幅简化代码
  4. 不能直接修改 props,必须通过事件通知父组件
  5. 支持多个 v-model,适合复杂表单组件

快速对比

vue 复制代码
<!-- 方式1:手动实现 -->
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
const handleInput = (e) => emit('update:modelValue', e.target.value)
</script>

<!-- 方式2:defineModel -->
<script setup>
const model = defineModel()
// 直接使用 model.value 读写
</script>

学习建议

  1. 先理解 v-model 的工作原理
  2. 掌握手动实现方式(理解底层)
  3. 在新项目中使用 defineModel(提高效率)
  4. 多写实际案例加深理解

参考资源


标签: Vue3, 双向绑定, v-model, defineModel, 组件通信

如果这篇文章对你有帮助,欢迎点赞、收藏、关注!有问题欢迎在评论区讨论。

相关推荐
C_心欲无痕2 小时前
使用 FNM (Fast Node Manager) 管理多个 Node.js 版本
前端·node.js
爬山算法2 小时前
Hibernate(44)Hibernate中的fetch join是什么?
前端·python·hibernate
橙序员小站2 小时前
解密前端包管理工具:npm、Yarn与pnpm的全面对比
前端·npm·node.js
m0_748254662 小时前
HTML DOM - 修改 HTML 内容的方法
前端·html
Jinuss2 小时前
React16与React17+的JSX转换差异
前端·react.js
爱吃香菜i2 小时前
数据连接开发设计文档
前端·javascript
冴羽2 小时前
现代 CSS 颜色使用指南
前端·javascript·css
Rrvive2 小时前
Vue3向全局广播数据变化
javascript·vue.js
cj81402 小时前
动态表单与静态表单性能比较
前端