🌟 前言:为什么要关注 defineModel

过去我们在 Vue 组件中使用 v-model 时,常需要这样写:

// 子组件
defineProps(['modelValue'])
defineEmits(['update:modelValue'])

function update(val) {
  emit('update:modelValue', val)
}

冗长、重复、繁琐,尤其在 TypeScript 中还不好维护。

自 Vue 3.4 起,defineModel() 这个宏让我们可以一行代码完成 v-model 的全部功能,甚至可以更强大地支持多个 v-model、修饰符、类型推导和默认值设置。


📦 一、基础用法详解

✅ 1. 声明默认 v-model(绑定 modelValue

// 子组件
const model = defineModel()

对应父组件:

<Child v-model="myValue" />

等价于原来的 props 和 emits:

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

watch(model, val => emit('update:modelValue', val))

✅ 2. 使用自定义 v-model 名称(比如 v-model:count)

const count = defineModel('count')

父组件这样用:

<Counter v-model:count="num" />

等价于:

defineProps(['count'])
defineEmits(['update:count'])

🎯 二、进阶用法:类型、默认值、必填

✅ 1. 指定类型(TypeScript 支持)

const username = defineModel<string>()

默认是 Ref<string | undefined>,如果希望一定有值:

const username = defineModel<string>({ required: true })
// Ref<string>

✅ 2. 设置默认值(但注意同步问题)

const age = defineModel<number>({ default: 18 })

🚨 注意: 父组件未传值时,子组件会用默认值,但父组件的值为 undefined,二者不同步

解决方案:

// 父组件初始化 ref 值
const myAge = ref(18)

🧩 三、多个 v-model 的支持(多个 defineModel)

Vue 3.4 支持多个 v-model

const checked = defineModel<boolean>()
const label = defineModel<string>('label')

对应父组件:

<Checkbox v-model="isChecked" v-model:label="labelText" />

这对于封装复选框、选择器、弹窗特别实用。


🪄 四、修饰符与转换器:.trim / .number / .uppercase

✅ 结构出修饰符

const [value, modifiers] = defineModel<string, 'trim'>()

父组件写:

<MyInput v-model.trim="inputValue" />

✅ 使用转换器处理值

const [value, modifiers] = defineModel<string, 'trim'>({
  set(val) {
    return modifiers.trim ? val.trim() : val
  }
})

也可以加自定义转换规则:

const [value, modifiers] = defineModel<string, 'uppercase'>({
  get(val) {
    return modifiers.uppercase ? val.toUpperCase() : val
  }
})

🔨 五、实战场景:封装可复用输入组件

<!-- BaseInput.vue -->
<script setup lang="ts">
const [value, modifiers] = defineModel<string, 'trim'>({
  set(val) {
    return modifiers.trim ? val.trim() : val
  }
})
</script>

<template>
  <input :value="value" @input="e => (value.value = e.target.value)" />
</template>

父组件中:

<BaseInput v-model.trim="username" />

这样封装的组件自动具备:

  • v-model 双向绑定
  • .trim 修饰符支持
  • 类型提示完善

🧠 六、常见坑位与注意事项

场景问题解决方案
使用 default父组件值未同步父组件初始化 ref 值
defineModel 无法识别泛型使用泛型时记得标注类型 <T>defineModel<string>()
旧项目报错低版本 Vue 不支持升级到 Vue 3.4+
修饰符未生效没有解构 modifiersconst [v, modifiers] = defineModel()

🧭 七、最佳实践总结

✅ 推荐场景:

  • 封装输入类组件:Input、Select、DatePicker
  • 封装复杂交互组件:弹窗、表格编辑单元
  • 封装通用控件库,提升可维护性

🚫 不推荐:

  • 低版本 Vue 项目(3.4 以下)
  • 和 setup 外逻辑混用(defineModel 仅支持 <script setup>

📘 附:对比 defineModel 与传统写法

项目defineModel传统写法
写法简洁✅ 一行定义❌ 需 props + emits
多个 v-model✅ 多次声明❌ 手动处理
类型提示✅ 泛型支持❌ 需额外定义 props 类型
默认值支持✅ 支持 default✅ 但仍需 emit
修饰符✅ 自动支持❌ 需手动处理

🧪 结语:如何开始使用?

  1. Vue 升级到 3.4+
npm install vue@^3.4
  1. 创建组件,使用 <script setup>defineModel

  2. 标准化写法,配合类型提示、修饰符使用,大幅提升组件封装质量和开发效率

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐