Vue 3 中 reactive 对象解构丢失响应性的真实原因与替代写法
在 Vue 3 项目里,不少人在组合式 API 中写 `const { count } = reactive({ count: 0 })` 之后,发现模板里 `count` 永远不会更新。这个问题通常不是 Vue 的 bug,而是 reactive 返回的是 Proxy 对象,解构本质上就是读取属性值再赋给新变量;如果属性是原始类型,新变量拿到的是一个普通值,和原来的 Proxy 已经断开联系。模板或 computed 再访问这个普通变量时,Vue 的依赖收集不会建立到原对象上,因此更新源对象也不会触发视图。常见场景包括把 reactive 表单字段解构给子组件、从 store 中解构状态、在组合式函数返回对象后直接解构使用。这篇文章会从 Proxy 的读取机制讲起,结合真实代码说明为什么解构后失效,并给出 toRefs、toRef、computed 以及直接保留对象引用的写法,同时对比 ref 和 reactive 在解构行为上的差异。 ## 问题复现 先看一段很常见的代码: ```vue {{ count }} ``` 点击按钮后,`state.count` 确实变了,但页面上的 `count` 不动。如果打开 Vue Devtools 观察,会发现组件并没有因为 `count` 变化而重新渲染。原因是 `const { count } = state` 执行时,`state.count` 被读取了一次,返回的是数字 `0`。这个 `0` 被复制到新变量 `count` 里,之后 `state.count++` 修改的是 Proxy 上的属性,和那个数字变量没有任何引用关系。 ## Proxy 的读取和依赖收集 Vue 3 的 `reactive` 基于 Proxy 实现。访问 `state.count` 时,会走到 handler 的 `get` 陷阱,Vue 在这里做两件事:返回当前值,同时把当前正在运行的 effect(比如组件的渲染 effect 或 computed effect)记录到 `count` 这个 key 的依赖集合里。 ```js const state = reactive({ count: 0 }) // 这一行发生在组件渲染函数里时,会收集依赖 template: () => h('div', state.count) ``` 解构发生在组件 setup 阶段,那时候渲染 effect 还没开始运行。更关键的是,解构之后模板里用的是局部变量 `count`,它已经不再是 Proxy 的属性访问,所以后续 `state.count++` 触发的是 Proxy 的 `set`,通知的是之前收集到的依赖,而模板根本没有订阅过这个局部变量。 ## toRefs 为什么能解决 `toRefs` 做的事是把 reactive 对象的每个属性转成 ref,并且这个 ref 的 `value` 访问会转发到原对象上: ```js import { reactive, toRefs } from 'vue' const state = reactive({ count: 0, name: 'vue' }) const { count, name } = toRefs(state) count.value++ // 等价于 state.count++ ``` 在模板里,Vue 会自动解包 ref,所以可以继续写 `{{ count }}`。这时候 `count` 是一个 ref 对象,模板访问 `count.value` 时会经过 ref 的 getter,而 ref 的 getter 内部再去读 `state.count`,依赖收集依然落在原来的 reactive 对象上。 toRefs 返回的是普通对象,不是 Proxy,但每个属性都是 ref。对原始类型属性来说,这是解构后保持响应性的标准做法。 ## toRef 只取单个属性 如果只关心一个字段,不需要把整个对象都转掉: ```js import { reactive, toRef } from 'vue' const state = reactive({ count: 0, name: 'vue' }) const count = toRef(state, 'count') count.value++ ``` `toRef` 和 `toRefs` 内部机制类似,都是创建一个与源对象属性双向联动的 ref。区别在于 `toRef` 针对单个 key,适合从 props 或 store 中提取某个字段。 ## 解构 props 的响应性 `defineProps` 返回的对象本身是响应式的,但直接解构同样会失去响应性: ```vue ``` Vue 3.5 之前,官方推荐用 `toRef` 或 `toRefs`: ```js import { toRef } from 'vue' const props = defineProps({ title: String }) const title = toRef(props, 'title') ``` Vue 3.5 开始,`defineProps` 返回的对象在解构时会保留响应性,编译器会识别解构并生成对应的 getter。如果项目锁在 3.4 或更早版本,还是需要手动 `toRef`。 ## reactive 嵌套对象解构的坑 嵌套对象解构后丢失响应性更隐蔽: ```js const state = reactive({ user: { name: 'a' } }) const { user } = state user.name = 'b' // 这个操作本身是响应式的,因为 user 还是 Proxy 对象 ``` 这里 `user` 仍然是 Proxy,所以修改 `user.name` 会触发更新。但如果再往下解构: ```js const { name } = state.user name = 'b' // 无效,name 是原始字符串 ``` 嵌套解构到原始类型那一层,响应性就断了。实际项目里常见于从表单对象中取出某个字段传给子组件,子组件内部再修改时发现父组件不更新。 ## ref 解构的差异 `ref` 本身是一个对象,解构它的 `.value` 也会丢失响应性: ```js const count = ref(0) const { value } = count value++ // 无效 ``` 但模板中 ref 会自动解包,所以直接写 `{{ count }}` 没问题。如果要把 ref 传给组合式函数再解构,必须保留 ref 本身,或者用 `toRef` 重新包一层。 ```js function useCounter() { const count = ref(0) return { count } } const { count } = useCounter() // 这里 count 还是 ref,没问题 ``` 组合式函数返回对象时,如果返回的是 reactive 对象,调用方解构就会出问题。所以很多库选择返回 ref 集合,而不是 reactive 对象。 ## 什么时候可以直接解构 如果解构出来的值只用于一次性计算,不参与后续更新,直接解构没问题: ```js const state = reactive({ list: [1, 2, 3] }) const { list } = state const total = list.reduce((a, b) => a + b, 0) ``` 只要 `list` 后续不会变化,或者变化后不需要重新计算,就不涉及响应性丢失。真正的坑在于把解构结果放进模板、computed 或 watch 里,期待它跟随源对象变化。 ## 在 store 中的实际写法 Pinia 的 store 返回的是 reactive 对象,直接解构 state 会失去响应性: ```js const store = useUserStore() const { name } = store // 失去响应性 ``` Pinia 提供了 `storeToRefs` 来处理: ```js import { storeToRefs } from 'pinia' const store = useUserStore() const { name } = storeToRefs(store) ``` `storeToRefs` 会跳过 actions,只为 state 和 getters 创建 ref。这样解构后模板里直接用 `{{ name }}`,修改用 `store.name = 'x'`,两边保持同步。 ## 一个容易忽略的 computed 场景 有人会写: ```js const state = reactive({ count: 0 }) const { count } = state const double = computed(() => count * 2) ``` `double` 不会随 `state.count` 更新,因为 computed 内部只读了普通变量 `count`,没有访问任何响应式属性。改成 `toRef` 后: ```js const count = toRef(state, 'count') const double = computed(() => count.value * 2) ``` computed 在第一次求值时读到 `count.value`,进入 ref 的 getter,再读到 `state.count`,依赖就被正确收集了。 ## 组合式函数返回值的取舍 写组合式函数时,返回 reactive 对象让调用方直接解构,通常是不友好的。更稳妥的做法是返回 ref: ```js export function useForm() { const form = reactive({ username: '', password: '' }) function reset() { form.username = '' form.password = '' } return { form, reset } } ``` 调用方要么保留 `form` 整体使用,要么用 `toRefs(form)` 解构。如果组合式函数明确只暴露几个字段,可以内部就转好: ```js export function useForm() { const form = reactive({ username: '', password: '' }) const { username, password } = toRefs(form) return { username, password, form } } ``` 这样调用方解构 `username` 时拿到的已经是 ref,不会踩坑。 ## 小结一下这个行为的记忆点 reactive 的解构丢失响应性不是设计缺陷,而是 JavaScript 值传递和 Proxy 依赖收集共同作用的结果。解构原始类型属性等于复制值,复制之后和源对象无关;解构嵌套对象属性时,如果该属性本身是 Proxy,那它还保持响应性,继续往下解构原始类型才会断。需要解构时,优先考虑 `toRefs`、`toRef`、`storeToRefs` 或直接访问原对象属性。模板里写 `state.count` 永远是最直接、最不容易出错的方式。 推荐查看天气:https://www.0dtq.com/jianning/ 推荐查看天气:https://www.0dtq.com/jianning/zuotian.html 推荐查看天气:https://www.0dtq.com/jianning/7tian.html 推荐查看天气:https://www.0dtq.com/jianning/10tian.html 推荐查看天气:https://www.0dtq.com/jianning/15tian.html 推荐查看天气:https://www.0dtq.com/jianning/40tian.html 推荐查看天气:https://www.0dtq.com/yongan/ 推荐查看天气:https://www.0dtq.com/yongan/zuotian.html 推荐查看天气:https://www.0dtq.com/yongan/7tian.html 推荐查看天气:https://www.0dtq.com/yongan/10tian.html 推荐查看天气:https://www.0dtq.com/yongan/15tian.html 推荐查看天气:https://www.0dtq.com/yongan/40tian.html 推荐查看天气:https://www.0dtq.com/quanzhou/ 推荐查看天气:https://www.0dtq.com/quanzhou/zuotian.html 推荐查看天气:https://www.0dtq.com/quanzhou/7tian.html 推荐查看天气:https://www.0dtq.com/quanzhou/10tian.html 推荐查看天气:https://www.0dtq.com/quanzhou/15tian.html 推荐查看天气:https://www.0dtq.com/quanzhou/40tian.html 推荐查看天气:https://www.0dtq.com/fengze/ 推荐查看天气:https://www.0dtq.com/fengze/zuotian.html 推荐查看天气:https://www.0dtq.com/fengze/7tian.html 推荐查看天气:https://www.0dtq.com/fengze/10tian.html 推荐查看天气:https://www.0dtq.com/fengze/15tian.html 推荐查看天气:https://www.0dtq.com/fengze/40tian.html 推荐查看天气:https://www.0dtq.com/quangang/ 推荐查看天气:https://www.0dtq.com/quangang/zuotian.html 推荐查看天气:https://www.0dtq.com/quangang/7tian.html 推荐查看天气:https://www.0dtq.com/quangang/10tian.html 推荐查看天气:https://www.0dtq.com/quangang/15tian.html 推荐查看天气:https://www.0dtq.com/quangang/40tian.html 推荐查看天气:https://www.0dtq.com/huian/ 推荐查看天气:https://www.0dtq.com/huian/zuotian.html 推荐查看天气:https://www.0dtq.com/huian/7tian.html 推荐查看天气:https://www.0dtq.com/huian/10tian.html 推荐查看天气:https://www.0dtq.com/huian/15tian.html 推荐查看天气:https://www.0dtq.com/huian/40tian.html 推荐查看天气:https://www.0dtq.com/anxixian/ 推荐查看天气:https://www.0dtq.com/anxixian/zuotian.html 推荐查看天气:https://www.0dtq.com/anxixian/7tian.html 推荐查看天气:https://www.0dtq.com/anxixian/10tian.html 推荐查看天气:https://www.0dtq.com/anxixian/15tian.html 推荐查看天气:https://www.0dtq.com/anxixian/40tian.html 推荐查看天气:https://www.0dtq.com/yongchun/ 推荐查看天气:https://www.0dtq.com/yongchun/zuotian.html 推荐查看天气:https://www.0dtq.com/yongchun/7tian.html 推荐查看天气:https://www.0dtq.com/yongchun/10tian.html 推荐查看天气:https://www.0dtq.com/yongchun/15tian.html 推荐查看天气:https://www.0dtq.com/yongchun/40tian.html 推荐查看天气:https://www.0dtq.com/dehua/ 推荐查看天气:https://www.0dtq.com/dehua/zuotian.html 推荐查看天气:https://www.0dtq.com/dehua/7tian.html 推荐查看天气:https://www.0dtq.com/dehua/10tian.html 推荐查看天气:https://www.0dtq.com/dehua/15tian.html 推荐查看天气:https://www.0dtq.com/dehua/40tian.html 推荐查看天气:https://www.0dtq.com/shishi/ 推荐查看天气:https://www.0dtq.com/shishi/zuotian.html 推荐查看天气:https://www.0dtq.com/shishi/7tian.html 推荐查看天气:https://www.0dtq.com/shishi/10tian.html 推荐查看天气:https://www.0dtq.com/shishi/15tian.html 推荐查看天气:https://www.0dtq.com/shishi/40tian.html 推荐查看天气:https://www.0dtq.com/jinjiang/ 推荐查看天气:https://www.0dtq.com/jinjiang/zuotian.html 推荐查看天气:https://www.0dtq.com/jinjiang/7tian.html 推荐查看天气:https://www.0dtq.com/jinjiang/10tian.html 推荐查看天气:https://www.0dtq.com/jinjiang/15tian.html 推荐查看天气:https://www.0dtq.com/jinjiang/40tian.html 推荐查看天气:https://www.0dtq.com/quanzhounanan/ 推荐查看天气:https://www.0dtq.com/quanzhounanan/zuotian.html 推荐查看天气:https://www.0dtq.com/quanzhounanan/7tian.html 推荐查看天气:https://www.0dtq.com/quanzhounanan/10tian.html 推荐查看天气:https://www.0dtq.com/quanzhounanan/15tian.html 推荐查看天气:https://www.0dtq.com/quanzhounanan/40tian.html 推荐查看天气:https://www.0dtq.com/zhangzhou/ 推荐查看天气:https://www.0dtq.com/zhangzhou/zuotian.html 推荐查看天气:https://www.0dtq.com/zhangzhou/7tian.html 推荐查看天气:https://www.0dtq.com/zhangzhou/10tian.html 推荐查看天气:https://www.0dtq.com/zhangzhou/15tian.html 推荐查看天气:https://www.0dtq.com/zhangzhou/40tian.html 推荐查看天气:https://www.0dtq.com/yunxiao/ 推荐查看天气:https://www.0dtq.com/yunxiao/zuotian.html 推荐查看天气:https://www.0dtq.com/yunxiao/7tian.html 推荐查看天气:https://www.0dtq.com/yunxiao/10tian.html 推荐查看天气:https://www.0dtq.com/yunxiao/15tian.html 推荐查看天气:https://www.0dtq.com/yunxiao/40tian.html 推荐查看天气:https://www.0dtq.com/zhangpu/ 推荐查看天气:https://www.0dtq.com/zhangpu/zuotian.html 推荐查看天气:https://www.0dtq.com/zhangpu/7tian.html 推荐查看天气:https://www.0dtq.com/zhangpu/10tian.html 推荐查看天气:https://www.0dtq.com/zhangpu/15tian.html 推荐查看天气:https://www.0dtq.com/zhangpu/40tian.html 推荐查看天气:https://www.0dtq.com/zhaoan/ 推荐查看天气:https://www.0dtq.com/zhaoan/zuotian.html 推荐查看天气:https://www.0dtq.com/zhaoan/7tian.html 推荐查看天气:https://www.0dtq.com/zhaoan/10tian.html 推荐查看天气:https://www.0dtq.com/zhaoan/15tian.html 推荐查看天气:https://www.0dtq.com/zhaoan/40tian.html 推荐查看天气:https://www.0dtq.com/changtai/ 推荐查看天气:https://www.0dtq.com/changtai/zuotian.html 推荐查看天气:https://www.0dtq.com/changtai/7tian.html 推荐查看天气:https://www.0dtq.com/changtai/10tian.html 推荐查看天气:https://www.0dtq.com/changtai/15tian.html 推荐查看天气:https://www.0dtq.com/changtai/40tian.html 推荐查看天气:https://www.0dtq.com/zhangzhoudongshan/ 推荐查看天气:https://www.0dtq.com/zhangzhoudongshan/zuotian.html 推荐查看天气:https://www.0dtq.com/zhangzhoudongshan/7tian.html 推荐查看天气:https://www.0dtq.com/zhangzhoudongshan/10tian.html 推荐查看天气:https://www.0dtq.com/zhangzhoudongshan/15tian.html 推荐查看天气:https://www.0dtq.com/zhangzhoudongshan/40tian.html 推荐查看天气:https://www.0dtq.com/nanjingxian/ 推荐查看天气:https://www.0dtq.com/nanjingxian/zuotian.html 推荐查看天气:https://www.0dtq.com/nanjingxian/7tian.html 推荐查看天气:https://www.0dtq.com/nanjingxian/10tian.html 推荐查看天气:https://www.0dtq.com/nanjingxian/15tian.html 推荐查看天气:https://www.0dtq.com/nanjingxian/40tian.html 推荐查看天气:https://www.0dtq.com/pinghe/ 推荐查看天气:https://www.0dtq.com/pinghe/zuotian.html 推荐查看天气:https://www.0dtq.com/pinghe/7tian.html 推荐查看天气:https://www.0dtq.com/pinghe/10tian.html 推荐查看天气:https://www.0dtq.com/pinghe/15tian.html 推荐查看天气:https://www.0dtq.com/pinghe/40tian.html 推荐查看天气:https://www.0dtq.com/huaan/ 推荐查看天气:https://www.0dtq.com/huaan/zuotian.html 推荐查看天气:https://www.0dtq.com/huaan/7tian.html 推荐查看天气:https://www.0dtq.com/huaan/10tian.html 推荐查看天气:https://www.0dtq.com/huaan/15tian.html 推荐查看天气:https://www.0dtq.com/huaan/40tian.html 推荐查看天气:https://www.0dtq.com/longhai/ 推荐查看天气:https://www.0dtq.com/longhai/zuotian.html 推荐查看天气:https://www.0dtq.com/longhai/7tian.html 推荐查看天气:https://www.0dtq.com/longhai/10tian.html 推荐查看天气:https://www.0dtq.com/longhai/15tian.html 推荐查看天气:https://www.0dtq.com/longhai/40tian.html 推荐查看天气:https://www.0dtq.com/nanping/ 推荐查看天气:https://www.0dtq.com/nanping/zuotian.html 推荐查看天气:https://www.0dtq.com/nanping/7tian.html 推荐查看天气:https://www.0dtq.com/nanping/10tian.html 推荐查看天气:https://www.0dtq.com/nanping/15tian.html 推荐查看天气:https://www.0dtq.com/nanping/40tian.html 推荐查看天气:https://www.0dtq.com/yanping/ 推荐查看天气:https://www.0dtq.com/yanping/zuotian.html 推荐查看天气:https://www.0dtq.com/yanping/7tian.html 推荐查看天气:https://www.0dtq.com/yanping/10tian.html 推荐查看天气:https://www.0dtq.com/yanping/15tian.html 推荐查看天气:https://www.0dtq.com/yanping/40tian.html 推荐查看天气:https://www.0dtq.com/shunchang/ 推荐查看天气:https://www.0dtq.com/shunchang/zuotian.html 推荐查看天气:https://www.0dtq.com/shunchang/7tian.html 推荐查看天气:https://www.0dtq.com/shunchang/10tian.html 推荐查看天气:https://www.0dtq.com/shunchang/15tian.html 推荐查看天气:https://www.0dtq.com/shunchang/40tian.html 推荐查看天气:https://www.0dtq.com/nanpingpucheng/ 推荐查看天气:https://www.0dtq.com/nanpingpucheng/zuotian.html 推荐查看天气:https://www.0dtq.com/nanpingpucheng/7tian.html 推荐查看天气:https://www.0dtq.com/nanpingpucheng/10tian.html 推荐查看天气:https://www.0dtq.com/nanpingpucheng/15tian.html 推荐查看天气:https://www.0dtq.com/nanpingpucheng/40tian.html 推荐查看天气:https://www.0dtq.com/guangze/ 推荐查看天气:https://www.0dtq.com/guangze/zuotian.html 推荐查看天气:https://www.0dtq.com/guangze/7tian.html 推荐查看天气:https://www.0dtq.com/guangze/10tian.html 推荐查看天气:https://www.0dtq.com/guangze/15tian.html 推荐查看天气:https://www.0dtq.com/guangze/40tian.html 推荐查看天气:https://www.0dtq.com/songxi/ 推荐查看天气:https://www.0dtq.com/songxi/zuotian.html 推荐查看天气:https://www.0dtq.com/songxi/7tian.html 推荐查看天气:https://www.0dtq.com/songxi/10tian.html 推荐查看天气:https://www.0dtq.com/songxi/15tian.html 推荐查看天气:https://www.0dtq.com/songxi/40tian.html 推荐查看天气:https://www.0dtq.com/zhenghe/ 推荐查看天气:https://www.0dtq.com/zhenghe/zuotian.html 推荐查看天气:https://www.0dtq.com/zhenghe/7tian.html 推荐查看天气:https://www.0dtq.com/zhenghe/10tian.html 推荐查看天气:https://www.0dtq.com/zhenghe/15tian.html 推荐查看天气:https://www.0dtq.com/zhenghe/40tian.html 推荐查看天气:https://www.0dtq.com/shaowu/ 推荐查看天气:https://www.0dtq.com/shaowu/zuotian.html 推荐查看天气:https://www.0dtq.com/shaowu/7tian.html 推荐查看天气:https://www.0dtq.com/shaowu/10tian.html 推荐查看天气:https://www.0dtq.com/shaowu/15tian.html 推荐查看天气:https://www.0dtq.com/shaowu/40tian.html 推荐查看天气:https://www.0dtq.com/wuyishan/ 推荐查看天气:https://www.0dtq.com/wuyishan/zuotian.html 推荐查看天气:https://www.0dtq.com/wuyishan/7tian.html 推荐查看天气:https://www.0dtq.com/wuyishan/10tian.html 推荐查看天气:https://www.0dtq.com/wuyishan/15tian.html 推荐查看天气:https://www.0dtq.com/wuyishan/40tian.html 推荐查看天气:https://www.0dtq.com/jianou/ 推荐查看天气:https://www.0dtq.com/jianou/zuotian.html 推荐查看天气:https://www.0dtq.com/jianou/7tian.html 推荐查看天气:https://www.0dtq.com/jianou/10tian.html 推荐查看天气:https://www.0dtq.com/jianou/15tian.html 推荐查看天气:https://www.0dtq.com/jianou/40tian.html 推荐查看天气:https://www.0dtq.com/jianyang/ 推荐查看天气:https://www.0dtq.com/jianyang/zuotian.html 推荐查看天气:https://www.0dtq.com/jianyang/7tian.html 推荐查看天气:https://www.0dtq.com/jianyang/10tian.html 推荐查看天气:https://www.0dtq.com/jianyang/15tian.html 推荐查看天气:https://www.0dtq.com/jianyang/40tian.html 推荐查看天气:https://www.0dtq.com/longyan/ 推荐查看天气:https://www.0dtq.com/longyan/zuotian.html