Code前端首页关于Code前端联系我们

Vue3里defineModel的deep选项咋用?这些场景和坑要留意!

terry 2小时前 阅读数 57 #Vue
文章标签 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-modeluser对象传给子组件:

<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.ageuser.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传对象 → 子组件开deepshallowRef只有在替换value时触发更新,嵌套属性修改不触发,所以子组件deep也监听不到。

简单说:子组件deep的效果,取决于父组件传递的响应式数据本身的“深度”,如果父组件传的是浅响应式数据,子组件开deep也没意义;只有父组件传的是深响应式数据(reactiveref包对象),子组件deep才能发挥作用。

原理层面:deep咋实现“深度响应”的?

Vue的响应式系统靠依赖收集+触发更新实现,当子组件用defineModel({ deep: true })接收对象/数组时,Vue会递归遍历这个数据的所有嵌套属性,给每个属性都建立“依赖关系”(可以理解为每个属性变化时,通知父组件更新)。

举个栗子:父组件传user对象,子组件开deep后,Vue会遍历user → user.detail → user.detail.address,给nameagecity这些属性都装上“监听器”,一旦子组件里改了city,监听器就会触发,告诉父组件“数据变了,快更新”。

要是没开deep,Vue只会给user这个顶层对象装监听器,只有替换整个user或者改user的直接属性(比如user.name)才会触发更新,嵌套的detailaddress这些属性的变化,监听器“看不到”,父组件自然不更新。

用deep的关键原则

defineModeldeep选项是把“双刃剑”:用对了能解决嵌套数据双向绑定的痛点,用错了会踩性能和响应式的坑,记住这几点,才能用得顺手:

  • 只在必须修改嵌套属性且需要父组件同步时开deep
  • 开之前先检查父组件传递的响应式数据类型(别和shallow系列冲突);
  • 性能敏感场景(比如大数据列表)尽量不用deep,换“替换整个对象/数组”的方式;
  • 写代码时多测试,看父组件是否真的同步更新了,避免逻辑错误。

把这些细节吃透,组件双向绑定才能更丝滑~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门