Vue3中defineModel怎么设置默认值?实际场景里要注意啥?
defineModel 是什么,解决了啥问题?
要搞懂 defineModel 设默认值的逻辑,得先明白它的作用,Vue 3.4 推出的 defineModel 是个编译宏,专门简化组件双向绑定开发,以前做子组件支持 v - model,得手动写 props 接收 modelValue,再用 emit 发 update:modelValue 通知父组件更新,步骤繁琐还容易漏,现在用 defineModel 就简单了——子组件里一行代码声明,自动处理 props 和 emit,返回的还是响应式 ref,修改值时能自动触发父组件更新,开发效率直接提升。
设置默认值的核心思路是啥?
给 defineModel 加默认值,要用到它的配置项。defineModel 第一个参数是绑定的“模型名”(比如父组件用 v - model:xxx,这里就填 xxx),第二个参数是对象,能配置 default、required 等规则,逻辑和 defineProps 配置 props 默认值类似。
举个例子:子组件想让绑定的“搜索关键词”默认显示“请输入”,代码可以这样写:
<script setup>
// 声明 model,名为 search,默认值设为 '请输入'
const search = defineModel('search', {
default: '请输入'
})
</script>
<template>
<input :value="search" @input="search = $event.target.value" />
</template>
父组件使用时,若不传值(如 <SearchInput v - model:search />),子组件里的 search 就用 '请输入' 这个默认值;若父组件传了值(如 <SearchInput v - model:search="parentValue" />),则优先用父组件给的 parentValue。
实际场景里设置默认值容易踩哪些坑?
步骤看着简单,实际开发稍不留意就会掉坑,这几个点得重点关注:
响应式与引用类型的“共享问题”
要是默认值是对象/数组这类引用类型,直接写 default: { name: '默认' } 有风险——所有子组件实例会共享同一个对象引用,一个组件改了属性,其他组件也会跟着变(因为内存里是同一个对象),这时得用工厂函数返回默认值,保证每个实例都是新对象:
const user = defineModel('user', {
default: () => ({ name: '匿名', age: 18 })
})
这样每个子组件的 user 默认值都是独立新对象,互不干扰。
父组件传值的“优先级”陷阱
要记准规则:父组件传了值,子组件默认值就不用;父组件没传(props 未被提供),才用默认值,但要是父组件传了 null 或者 undefined 呢?比如父组件写 <Child v - model:xxx="null" />,这时子组件会不会用默认值?实测发现,Vue 会把 null/undefined 当作“传了值”,默认值不会生效,要是业务里需要“传 null 时用默认值”,得在子组件里手动判断处理:
const xxx = defineModel('xxx', { default: '默认' })
// 手动处理:若父组件传了 null/undefined,强制用默认值
watch(xxx, (newVal) => {
if (newVal == null) {
xxx.value = '默认'
}
})
v - model 修饰符的“类型冲突”
父组件用 v - model 修饰符时(.number 把输入转数字,.trim 去空格),子组件默认值的类型得和修饰符逻辑匹配,比如父组件用 <Child v - model:age.number="parentAge" />,子组件 age 的默认值得是 0(数字),别设成字符串 '0',否则可能出现类型不兼容引发的逻辑错误。
和传统 v - model 实现方式对比,默认值处理有啥不同?
以前写双向绑定,子组件得手动写 props 和 emit:
<!-- 传统写法:子组件 -->
<script setup>
const props = defineProps({
modelValue: {
type: String,
default: '请输入'
}
})
const emit = defineEmits(['update:modelValue'])
const handleInput = (e) => {
emit('update:modelValue', e.target.value)
}
</script>
<template>
<input :value="modelValue" @input="handleInput" />
</template>
现在用 defineModel,代码量直接减半,默认值配置逻辑和 defineProps 类似,但更简洁:
<!-- 新写法:子组件 -->
<script setup>
const model = defineModel('modelValue', {
default: '请输入'
})
const handleInput = (e) => {
model.value = e.target.value // 自动触发 emit
}
</script>
<template>
<input :value="model" @input="handleInput" />
</template>
核心差异是响应式处理更自动化:defineModel 返回的是 ref,修改 value 时自动发 emit;传统写法得手动调用 emit,而且代码结构更紧凑,少了 props 和 emit 的重复声明,降低了理解成本。
举个完整案例,从父到子设置默认值
以“搜索组件”场景为例,子组件 SearchInput 要支持默认提示文字,父组件可传可不传,同时输入变化要同步给父组件。
子组件 SearchInput.vue:
<template>
<div class="search - wrap">
<input
class="search - input"
:value="searchValue"
@input="onInput"
placeholder="(这里用默认值兜底)"
/>
<button @click="clear">清空</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 声明 model,名为 search,默认值是 '请输入关键词'
const searchValue = defineModel('search', {
default: '请输入关键词'
})
// 输入时更新 model
const onInput = (e) => {
searchValue.value = e.target.value
}
// 清空逻辑:直接改 model 的值,自动通知父组件
const clear = () => {
searchValue.value = ''
}
</script>
<style scoped>
.search - wrap { display: flex; }
.search - input { flex: 1; }
</style>
父组件 Parent.vue:
<template>
<div>
<!-- 情况1:父组件传值,子组件用父组件的 value -->
<SearchInput v - model:search="parentSearch" />
<p>父组件同步的值:{{ parentSearch }}</p>
<!-- 情况2:父组件不传值,子组件用默认值 '请输入关键词' -->
<SearchInput v - model:search />
</div>
</template>
<script setup>
import { ref } from 'vue'
import SearchInput from './SearchInput.vue'
// 父组件自己的响应式变量
const parentSearch = ref('')
</script>
运行后能看到:第一个 SearchInput 用父组件的 parentSearch(初始空字符串),第二个因父组件没传,自动显示“请输入关键词”;输入或点“清空”时,子组件修改 searchValue.value 会自动同步给父组件,双向绑定逻辑全由 defineModel 接管,不用手动写 emit,十分便捷~
团队协作时关于默认值的规范建议
多人协作开发组件,默认值处理不统一易留隐患,这几个规范能避坑:
组件文档写清默认值逻辑
在组件的注释/文档里,明确写出 defineModel 对应的“模型名”和默认值。
<!-- SearchInput.vue 文档注释 --> /** * 搜索输入框组件 * @model search - 双向绑定的搜索关键词 * @default search - 未传值时,默认显示 '请输入关键词' * @example <SearchInput v - model:search="parentVal" /> - 父组件传值 * @example <SearchInput v - model:search /> - 用默认值 */
其他人用组件时,看文档就知不传值的行为,减少沟通成本。
严格保持类型一致性
默认值的类型要和父组件预期类型一致,比如父组件用 v - model:age.number 期望传数字,子组件 age 的默认值就得是 0(数字类型),别写成 '0'(字符串),否则可能出现“输入框输数字,父组件拿到字符串”这类奇怪 Bug。
复杂默认值用工厂函数
若默认值是对象、数组这类引用类型,必须用工厂函数返回默认值(如前面讲的 default: () => ({ ... })),确保每个组件实例的默认值都是独立对象,避免“一个组件改值,其他组件跟着变”的共享问题。
defineModel 设置默认值的关键是利用第二个参数的 default 配置项,同时要留意引用类型的独立初始化、父组件传值优先级、类型与修饰符匹配这些要点,结合案例和规范,既能写得轻松(代码少),又能运行稳定(逻辑无错),团队协作也更顺畅~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


