Vue3里defineModel的deep选项咋用?这些场景和坑要留意!
先搞懂defineModel和deep是干啥的?
咱先回忆下,Vue3.4之后推出的defineModel,是个简化组件双向绑定的语法糖,以前写v-model双向绑定,得同时用defineProps接收modelValue,再用defineEmits触发update:modelValue,现在用defineModel一行代码就能搞定双向绑定逻辑。
但如果父组件传给子组件的是对象、数组这类复杂数据,问题就来了:默认情况下,只有修改数据的“顶层结构”(比如替换整个对象、给数组换个新元素),父组件才会更新;要是改的是嵌套属性(比如对象里的对象、数组里的对象的某个属性),父组件压根没反应,这时候deep选项就派上用场了——开启deep后,子组件里修改嵌套属性,父组件也能跟着更新。
啥场景下必须开deep?
举几个实际开发中常见的情况,你就懂啥时候非开deep不可了:
表单里的深层字段编辑
比如做个用户信息弹窗,父组件传个user对象:
{
name: '张三',
detail: { age: 18, address: { city: '北京' } }
}
子组件里要改user.detail.address.city,这时候如果没开deep,改完城市名父组件的user对象不会更新,页面也不刷新,必须开deep,才能让父组件同步变化。
树形结构/层级数据操作
比如做个权限树,每个节点有children数组,子组件里修改某个子节点的选中状态(比如node.children[0].checked = true),这种嵌套结构的修改,必须靠deep才能让父组件的树数据实时更新。
动态配置项的嵌套修改
比如后台配置一个表单的动态规则,规则是个嵌套对象:
{
rule: {
type: 'input',
props: { placeholder: '请输入' }
}
}
子组件里改props.placeholder,父组件得同步,这时候deep必须开。
但注意哦!如果只是改简单数据(比如字符串、数字),或者只需要替换整个对象/数组(比如给数组push新元素后用深拷贝替换),那没必要开deep,反而会影响性能。
代码里咋配置deep?实操例子走一个
光说不练假把式,咱写两段代码,对比开deep和不开deep的区别。
父组件(Parent.vue)
先写父组件,用v-model把user对象传给子组件:
<template>
<div>
父组件用户信息:{{ user }}
<Child v-model="user" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const user = ref({
name: '张三',
detail: {
age: 18,
address: { city: '北京' }
}
})
</script>
子组件(Child.vue):开deep的情况
子组件里用defineModel({ deep: true })接收数据,修改嵌套属性:
<template>
<div>
子组件修改年龄:<button @click="changeAge">改年龄</button>
子组件修改城市:<button @click="changeCity">改城市</button>
</div>
</template>
<script setup>
const model = defineModel({ deep: true }) // 关键:配置deep为true
const changeAge = () => {
model.value.detail.age = 20 // 改嵌套的age
}
const changeCity = () => {
model.value.detail.address.city = '上海' // 改嵌套的city
}
</script>
点击按钮后,父组件的user.detail.age和user.detail.address.city都会更新,页面也会重新渲染。
子组件(Child.vue):不开deep的情况
把defineModel({ deep: true })改成defineModel(),再点按钮,父组件的user对象不会变——因为只改了嵌套属性,没触发顶层更新,这时候只有把整个detail对象替换(比如model.value.detail = { ...model.value.detail, age: 20 }),父组件才会更新,但这样写代码很麻烦,不如开deep方便。
用deep时容易踩的“暗坑”有哪些?
deep虽好,但用错了也容易掉坑里,这几个常见问题得留意:
性能问题:深度监听很“吃性能”
如果父组件传的对象特别大(比如有几十层嵌套、几百个属性),开deep后Vue要递归遍历所有嵌套属性,给每个属性加响应式监听,这会让初始化和更新时的性能变差,所以非必要别开,比如只是改顶层就够的场景,坚决不开。
响应式“丢了”:手动改数据结构容易翻车
比如子组件里直接把model.value整个替换成新对象:
model.value = { name: '李四', detail: { ... }}
这时候哪怕开了deep,也得确保新对象是响应式的(比如用reactive包一下),否则新对象的嵌套属性修改时,父组件还是不更新。
父组件绑定方式不对:deep白开了
defineModel基于v-model双向绑定,如果父组件没写v-model,而是用modelValue="user"单向传值,那子组件里不管开不开deep,改数据都不会触发父组件更新——因为双向绑定的前提是v-model(即同时传值+监听update事件),这时候得检查父组件是不是用对了v-model。
和shallowReactive、shallowRef“打架”
如果父组件传的是shallowReactive(user)(浅响应式对象,只有顶层属性有响应式),那子组件开deep也没用——因为源数据本身就没给嵌套属性加监听,子组件再怎么deep也“监听不到”,同理,父组件用shallowRef包对象,子组件deep也白搭。
和其他响应式API结合要注意啥?
父组件传递数据时用的响应式API不同,子组件deep的效果也不一样,咱分情况看:
- 父组件用
reactive传对象 → 子组件开deep:能监听到所有嵌套属性变化,没问题。 - 父组件用
shallowReactive传对象 → 子组件开deep:没用,因为shallowReactive只给顶层加监听,嵌套属性还是普通对象,子组件deep监听不到。 - 父组件用
ref传对象(ref.value是对象) → 子组件开deep:等价于reactive的情况,能深度监听。 - 父组件用
shallowRef传对象 → 子组件开deep:shallowRef只有在替换value时触发更新,嵌套属性修改不触发,所以子组件deep也监听不到。
简单说:子组件deep的效果,取决于父组件传递的响应式数据本身的“深度”,如果父组件传的是浅响应式数据,子组件开deep也没意义;只有父组件传的是深响应式数据(reactive或ref包对象),子组件deep才能发挥作用。
原理层面:deep咋实现“深度响应”的?
Vue的响应式系统靠依赖收集+触发更新实现,当子组件用defineModel({ deep: true })接收对象/数组时,Vue会递归遍历这个数据的所有嵌套属性,给每个属性都建立“依赖关系”(可以理解为每个属性变化时,通知父组件更新)。
举个栗子:父组件传user对象,子组件开deep后,Vue会遍历user → user.detail → user.detail.address,给name、age、city这些属性都装上“监听器”,一旦子组件里改了city,监听器就会触发,告诉父组件“数据变了,快更新”。
要是没开deep,Vue只会给user这个顶层对象装监听器,只有替换整个user或者改user的直接属性(比如user.name)才会触发更新,嵌套的detail、address这些属性的变化,监听器“看不到”,父组件自然不更新。
用deep的关键原则
defineModel的deep选项是把“双刃剑”:用对了能解决嵌套数据双向绑定的痛点,用错了会踩性能和响应式的坑,记住这几点,才能用得顺手:
- 只在必须修改嵌套属性且需要父组件同步时开
deep; - 开之前先检查父组件传递的响应式数据类型(别和
shallow系列冲突); - 性能敏感场景(比如大数据列表)尽量不用
deep,换“替换整个对象/数组”的方式; - 写代码时多测试,看父组件是否真的同步更新了,避免逻辑错误。
把这些细节吃透,组件双向绑定才能更丝滑~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



