本文探讨了Vue 3中`ref`与`reactive`在实现双向数据绑定时的区别与应用场景。用户在使用`reactive`定义数据并通过`v-model`在子组件中传递时,遇到了双向绑定失效的问题。分析代码发现,子组件将父组件传来的`reactive`对象通过扩展运算符`{...}`复制,生成了一个新的非响应式对象,导致了数据更新的脱节。文章将深入解析`ref`和`reactive`在响应式原理上的差异,并指导开发者何时应选择`ref`,何时应选择`reactive`,以确保数据流的顺畅与组件间的有效通信。
✨ **`reactive`对象在子组件中的传递与更新陷阱**: 在Vue 3中,当使用`reactive`创建的数据对象传递给子组件时,若子组件通过扩展运算符`{...props.modelValue}`来复制该对象(如示例中的`localConfig`),则会创建一个新的、独立的普通JavaScript对象。这个新对象不再具有`reactive`的响应式追踪能力,因此在子组件中修改它(例如,通过input框修改`localConfig.fontSize`)不会自动触发父组件的`v-model`更新,因为`emit('update:modelValue', { ...newConfig })`发送的是一个普通对象,且`localConfig`本身与父组件的`reactive`对象是分离的。
💡 **`ref`在处理`v-model`时的直接性**: `ref`创建的数据(包括对象)会暴露一个`.value`属性来访问其值。当`v-model`绑定到`ref`的`.value`时,父子组件的数据传递和更新更加直接。例如,如果`styleConfig`在父组件中是`ref({ fontSize: 16 })`,子组件可以直接绑定到`props.modelValue.value`,修改时通过`emit('update:modelValue', { value: newSize })`(或更常见的,使用计算属性)即可实现双向绑定,因为`.value`的修改会被`ref`捕获并触发更新。
🚀 **正确处理`reactive`数据的子组件双向绑定**: 要使`reactive`数据在子组件中通过`v-model`实现双向绑定,应避免直接复制。更推荐的做法是利用计算属性(`computed`)来创建可读写的getter和setter。在getter中返回`props.modelValue`,在setter中触发`emit('update:modelValue', newValue)`,这样子组件的修改就能正确地映射回父组件的`reactive`数据。
✅ **`ref`的应用场景**: `ref`最适合用于声明**基本数据类型**(如字符串、数字、布尔值)的状态,因为它们的值是独立的,直接通过`.value`访问和修改即可。对于**对象或数组**,`ref`也可以使用,特别是在你需要**重新赋值整个对象或数组引用**时,`ref`的`.value`机制能够正确地追踪到这个引用变化,并触发响应式更新。
📦 **`reactive`的应用场景**: `reactive`是处理**复杂对象和数组**的首选。它将整个对象转化为Proxy代理,使得对象内部的属性变更都能被Vue追踪。当你需要深度响应式地监听和修改一个对象的所有属性时,`reactive`是更自然的选择,且通常用于存储组件的内部状态,避免了`.value`的层层访问。
萌新小白请教,不太明白为什么使用 ref 数据可以双向绑定,使用 reactive 数据无法双向绑定?是我用法不对吗?
到底什么时候应该使用 ref 什么时候应该使用 reactive 呢?
https://play.vuejs.org/#eNp9U8Fu00AQ/ZXRXmJEsIVAHCInElQ9wAEqijj54tqTdMt6d2Wv3UCUOzeEOMAHIHFC8AP8De13MDubpNu09GLtzrx582b9ZiWeWpsOPYqJyLuqldZBh663s0LLxprWwQpanI/pU1ZODghrmLemgREVjXagA9PYTTzN/MVzUrrQWQaV0R3RuvcKD4yeywVMPWey4izMjXbH8gNO4OETjqzvFfr2miDBF+6X+Zo8CxOQdro4bKwqHdINILcwYbJpIVZRadQg3UbhPozscrQuxOzy4+fLb78ufn79++dLnvlHISqedXjQmBoV0UUUhYCMMHkW9RZj4Tqaxnc464ymh2b5haiIRypsX1knadpCTIAzPlcqZc5fcMy1PY638eoUq3e3xM+6pY8V4qjFDtsBC7HLubJdoAvpw+OXuKTzLklT9IrQdyRfY2dU7zUG2LNe1yQ7wrHa5+wEqRdvusOlQ91th/JCPXLN+EKQM/wT/m/0K7mP0sdcR3+XXnHrqn2ngir1gv6DI5rrrg1uGcN56arTPd8SUDts52WFcBy5bM9aum9OaFRNGuKKo9bYLmDZBm9L1RM6IuKSYGLL4CnUOJcauTTn7ywh08a0h410G9qE6Ea9rclEk6sWozEMN1rdm8BgZB11ROLZNWTSnL+bhgGkTFWqG7uVR8SzZAVpmrL+9EoE72eh+VWTiGUMicbzjSaYzsIgXkty6yRMvqtg1rWP1og2uIZjd211LQc+0FGVJ6hmYVUvvv+4+P0pz0JsA5Da9i7a20j5bvU3C0zoLFBfX+X1P0Ky0Q8=
直接展示代码:
App.vue
<script setup>import { ref, reactive } from 'vue'import Comp from './Comp.vue'// const styleConfig = ref({// fontSize: 16// })const styleConfig = reactive({ fontSize: 16})</script><template> <p :style="{ fontSize: styleConfig.fontSize + 'px'}">我是字体</p> <Comp v-model="styleConfig" /></template>
Comp.vue
<script setup lang="ts">import { reactive, watch } from 'vue'interface StyleConfig { fontSize: number}interface Props { modelValue: StyleConfig}const props = defineProps<Props>()interface Emits { (e: 'update:modelValue', value: StyleConfig): void}const emit = defineEmits<Emits>()const localConfig = reactive<StyleConfig>({ ...props.modelValue })watch(localConfig, (newConfig) => { emit('update:modelValue', { ...newConfig })}, { deep: true })</script><template> <div> <label>字体大小</label> <input v-model="localConfig.fontSize" /> </div></template>