reactive 响应式丢失的排查
最近,在使用vue3开发中遇到一个响应式丢失的场景:对一个用reactive声明的对象通过工具函数处理过后,页面没有同步更新数据,排查了很久,才发现是对对象进行解构和重新赋值后,导致了响应式的丢失,例举如下demo来复现一下上述问题:
ini
<script setup lang="ts">
import { reactive, ref } from "vue";
let stateReactive = reactive({
count: 0,
});
const anotherState = reactive({
message: "Hello Vue 3!",
});
const res = { ...stateReactive, ...anotherState };
function changeRes() {
stateReactive.count++;
anotherState.message = "Hello Vue 3 - Updated!";
}
function changeState() {
stateReactive = {
count: 3,
};
}
</script>

如上述图片所示,无论如何点击按钮,页面中的数据都没有发生改变,可见这两组数据响应式出现了丢失的情况。
reactive响应式丢失场景汇总
一举反三,接下来我来汇总一下reactive会丢失响应式的一些场景,帮助大家避免踩坑。
1. 解构赋值导致丢失
php
const state = reactive({ count: 0, user: { name: 'Tom' } })
// ❌ 直接解构会丢失响应式
const { count } = state
console.log(count) // 0,但不是响应式了
// ✅ 解决:使用 toRefs / toRef
const { count } = toRefs(state)
2. 返回新对象覆盖
ini
const state = reactive({ list: [1, 2, 3] })
// ❌ 重新赋值整个对象,响应式丢失
state = { list: [4, 5, 6] }
// ✅ 正确做法:修改属性而不是替换对象
state.list = [4, 5, 6]
3. reactive 包裹的对象被 JSON.parse / JSON.stringify 处理
javascript
const state = reactive({ user: { name: 'Tom' } })
// ❌ 转换后失去响应式
const newState = JSON.parse(JSON.stringify(state))
// ✅ 如果需要深拷贝,保留响应式,可以用结构化 clone + reactive
const newState = reactive(structuredClone(state))
4. 数组或对象直接解构/赋值
php
const state = reactive({ arr: [1, 2, 3] })
// ❌ 解构数组丢失响应式
const arr = state.arr
arr.push(4) // 不会触发视图更新
// ✅ 使用 toRef 或者始终通过 state.arr 修改
const arr = toRef(state, 'arr')
5. 使用浅拷贝
xml
<script setup>
const state = reactive({ user: { name: 'Tom' } })
const userCopy = { ...state.user } // ❌ userCopy 不是响应式
</script>
✅ 正确做法:直接用 state.user,或者 toRefs(state.user)
6. reactive
不能嵌套使用
php
const state = reactive({ user: reactive({ name: 'Tom' }) })
// ❌ 内层 reactive 会被 unwrap 掉,丢失预期响应式行为
正确做法:只用一次 reactive
,嵌套对象内部会自动递归代理。
ref是否也会丢失响应式
是的,ref也会丢失响应式的,ref
包裹对象 和 reactive
包裹对象的知识表现不同,对ref包裹的对象进行解构依然会出现响应式丢失的情况。
xml
<script setup lang="ts">
import { reactive, ref } from "vue";
const stateRef = ref({
count: 0,
});
function changeState() {
stateReactive = {
count: 3,
};
}
const resRef = { ...stateRef.value };
function changeResRef() {
resRef.count++;
}
</script>
<template>
<div class="card">
<button type="button" @click="changeResRef">{{ resRef }}</button>
</div>
</template>
这里的表现与上述图片一样,点击按钮,按钮中的数据不会发生变化。
ref和reactive丢失响应式场景对比
场景 | reactive | ref(对象) | 具体例子 |
---|---|---|---|
解构属性 | ❌ 丢失 | ❌ 丢失 | const state = reactive({ count: 0 })const { count } = state // ❌ 非响应式const obj = ref({ count: 0 })const { count } = obj.value // ❌ 非响应式\n |
替换整个对象 | ❌ 丢失响应式 | ✅ 推荐做法 | const state = reactive({ a: 1 })state = { a: 2 } // ❌ 丢失响应式const obj = ref({ a: 1 })obj.value = { a: 2 } // ✅ 保持响应式\n |
基本类型 | ❌ 不支持 | ✅ 支持 | const num = reactive(0) // ❌ 无效const num = ref(0) // ✅ 正常\n |
JSON.parse / 拷贝 | ❌ 丢失 | ❌ 丢失 | const state = reactive({ a: 1 })const copy1 = JSON.parse(JSON.stringify(state))copy1.a = 2 // ❌ 不触发更新const obj = ref({ a: 1 })const copy2 = JSON.parse(JSON.stringify(obj.value))copy2.a = 2 |
数组解构 | ❌ 丢失 | ❌ 丢失 | const state = reactive({ list: [1,2,3] })const list1 = state.listlist1.push(4)const obj = ref({ list: [1,2,3] })const list2 = obj.value.listlist2.push(4) // ❌ 不触发更新\n |
props 传递 | 可能丢失 | 可能丢失 | // 父组件\n<Child :data="state" />// 子组件const { data } = defineProps<{ data: any }>()// ❌ 解构 data 后丢失响应式\n |
避免响应式丢失的方法
常见的解决方案有:
- 使用
toRef
/toRefs
保持解构后的响应式
scss
const state = reactive({ count: 0 });
const { count } = toRefs(state);
setInterval(() => {
state.count++;
console.log("响应式 count:", count.value); // ✅ 会更新
}, 1000);
- reactive声明的对象,需要指定修改对象属性,而不是整体覆盖(除非用
ref
包裹) - 避免
JSON.stringify / parse
破坏响应式 - 在组件中不要直接解构
props
,配合toRefs
使用(重要)
xml
<!-- Parent.vue -->
<Child :user="user" />
<script setup>
import { reactive } from "vue";
import Child from "./Child.vue";
const user = reactive({ name: "张三" });
</script>
<!-- Child.vue -->
<script setup>
import { toRefs } from "vue";
const props = defineProps({ user: Object });
const { name } = toRefs(props.user);
// ✅ 响应式 name
</script>
ref 与 reactive 的实践建议
使用建议
- 小数据 / 基本类型 → ref
- 复杂对象(状态树、多属性) → reactive
- 需要整体替换的对象 → ref
- 解构属性时 → 配合 toRefs