严格来说,Vue 3 并没有在某个版本中彻底"禁止"在 prop 上使用 v-model,而是从 Vue 3.4 版本开始,改变了推荐的实现方式,并加强了对旧有错误用法的限制。
这一变化主要与新的编译宏 defineModel 的引入有关。
✨ Vue 3.4 之前的传统写法
在 Vue 3.4 之前,要在自定义组件上实现 v-model,开发者需要手动完成以下两步:
- 声明一个名为
modelValue的 prop(或使用参数的自定义名称,如title)。 - 在需要更新值时,通过
emit('update:modelValue', newValue)触发事件。
这是一种"语法糖",虽然有效,但需要编写不少样板代码。
🚀 Vue 3.4 及之后的新变化
从 Vue 3.4 版本开始,Vue 官方引入了 defineModel 宏,旨在简化这一过程。
- 新的推荐方式 :使用
defineModel()。它会自动帮你完成 prop 的声明和update:modelValue事件的触发,无需手动编写。这让代码更简洁,但也意味着旧的、手动的写法不再是官方推荐的主流。 - 报错原因 :如果你在 Vue 3.4+ 的项目中,仍然沿用旧的思维,试图直接在一个通过
defineProps声明的 prop 上使用v-model(例如v-model="modelValue"),Vue 的编译器现在会抛出一个错误:v-model cannot be used on a prop, because local prop bindings are not writable这是因为 prop 是单向数据流,不可直接修改。新版本更希望你使用
defineModel来获得一个可写的响应式引用。
总结
- Vue 3.4 之前:必须手动声明 prop 和 emit。
- Vue 3.4 及之后 :推荐使用
defineModel宏。继续使用旧的手动方式可能会因为直接修改 prop 而导致报错。
因此,你遇到的问题很可能是项目升级到 Vue 3.4+ 后,代码未同步更新为新规范导致的。
defineModel怎么用?
defineModel 是 Vue 3.4+ 版本引入的一个非常实用的宏(Macro),它能极大地简化父子组件之间的双向绑定逻辑。
简单来说,它把过去需要通过 defineProps 和 defineEmits 配合才能实现的双向绑定,封装成了一行代码。
以下是 defineModel 的具体使用方法,从基础到进阶:
1. 基础用法:默认模型
这是最简单的场景,用于替代传统的 v-model。
-
子组件 (Child.vue):
只需调用
defineModel(),它会返回一个响应式引用,该引用自动与父组件的值同步。<script setup> // 1. 调用 defineModel,无需导入 const modelValue = defineModel() // 2. 直接在模板中使用,就像使用 ref 一样 // 修改它会自动触发 update:modelValue 事件 </script> <template> <!-- 直接双向绑定 --> <input v-model="modelValue" placeholder="输入内容..." /> <!-- 或者在 JS 中修改 --> <!-- modelValue.value = '新值' --> </template> -
父组件 (Parent.vue):
使用方式与传统
v-model完全一致,无需改变。<script setup> import { ref } from 'vue' import Child from './Child.vue' const inputValue = ref('') </script> <template> <!-- 绑定数据 --> <Child v-model="inputValue" /> <p>当前值: {{ inputValue }}</p> </template>
2. 进阶用法:自定义名称
如果你需要多个 v-model,或者想给模型起个特定的名字(比如 username),可以传入一个字符串参数。
-
子组件 (UserProfile.vue):
<script setup> // 定义两个不同的模型 const username = defineModel('username') const age = defineModel('age') </script> <template> <input v-model="username" placeholder="用户名" /> <input v-model.number="age" type="number" placeholder="年龄" /> </template> -
父组件:
<script setup> import { ref } from 'vue' import UserProfile from './UserProfile.vue' const myName = ref('') const myAge = ref(0) </script> <template> <!-- 使用 v-model:名称 的形式 --> <UserProfile v-model:username="myName" v-model:age="myAge" /> </template>
3. 高级配置:类型、默认值与校验
defineModel 还支持传入配置项,这在 TypeScript 项目中非常有用。
-
设置类型 (TypeScript):
// 指定类型为 string const text = defineModel<string>() // 指定为必填,这样类型推导会更精确(非 undefined) const requiredText = defineModel<string>({ required: true }) -
设置默认值:
// 如果父组件没传值,默认使用 'hello' const message = defineModel({ default: 'hello' })注意:如果设置了默认值,建议父组件也初始化对应的 ref,以避免初始渲染时的值不同步问题。
-
设置校验规则:
const phone = defineModel('phone', { type: String, required: true, validator: (value) => /^1[3-9]\d{9}$/.test(value) // 简单手机号校验 })
4. 处理修饰符 (.trim, .number 等)
defineModel 还能感知父组件传递的修饰符,比如 v-model.trim。
-
子组件 (InputWithModifiers.vue):
<script setup> // 结构出值和修饰符对象 const [value, modifiers] = defineModel(['trim', 'uppercase']) // 你可以根据 modifiers 对象来决定如何处理值 // 例如:如果用了 .trim 修饰符,自动去除空格 </script> <template> <!-- 这里只是一个示例,通常你不需要手动处理,v-model 会自动处理 .trim 和 .number --> <input v-model="value" /> </template> -
父组件:
<!-- .trim 和 .number 是 Vue 内置的修饰符,会自动生效 --> <!-- uppercase 是上面代码中自定义的修饰符名称 --> <InputWithModifiers v-model.trim.uppercase="inputText" />
总结
使用 defineModel 的核心优势在于减少样板代码 。过去你需要写 props 和 emit,现在只需要一行 const model = defineModel(),它本质上是一个语法糖,内部自动帮你生成了响应式引用和更新逻辑。