Vue3 响应式陷阱:解构赋值后页面不动了?Proxy 的"隐形成员"在搞鬼

问题场景

用 Vue3 Composition API 写了一个商品列表组件:

vue 复制代码
<script setup>
import { ref, reactive, onMounted } from 'vue'

const state = reactive({
  list: [],
  loading: false,
  page: 1,
  total: 0
})

// ❌ 常见操作:解构出来用
const { list, loading, page, total } = state

const loadMore = () => {
  page.value++  // ❌ 报错:page is not a ref
  console.log(page) // 输出 1,但页面不会更新
}

onMounted(async () => {
  const res = await fetch('/api/goods?page=1')
  const data = await res.json()
  list.push(...data.list)  // ✅ 数组变了
  // ❌ 但页面没渲染!
  total = data.total        // ❌ 页面也没更新
  loading = false           // ❌ 页面还是 loading 状态
})
</script>

代码逻辑自认为都对,但 页面纹丝不动。total 从 0 变 100,loading 从 true 变 false,渲染层永远停留在初始状态。

原因分析

Vue3 的 reactive 基于 Proxy 实现响应式 。Proxy 拦截的是对原始代理对象 的属性读写。当你 const { list, loading } = state 解构时:

  1. 读取发生在解构那一刻 --- list 拿到的是 state.list当前值副本,而不是一个"活的"引用
  2. 后续对 list.push() 之所以有效,是因为 state.list 本身是数组(引用类型),list 持有同一个 Proxy 数组的引用,改数组内容触发 Proxy 的 set 拦截
  3. totalloading 是原始值 (number/boolean),解构后变成普通的 JS 变量,完全脱离了 Proxy 的追踪
  4. 对普通变量的赋值 total = data.total 只是在给局部变量重新绑定,没有触发任何 set 拦截

总结一句:reactive 解构基本类型 = 自动丧失响应式

解决方案

方案一:不解构,直接用 state.xxx

vue 复制代码
<script setup>
const state = reactive({
  list: [],
  loading: false,
  page: 1,
  total: 0
})

const loadMore = async () => {
  state.page++
  const res = await fetch(`/api/goods?page=${state.page}`)
  const data = await res.json()
  state.list.push(...data.list)
  state.total = data.total
  state.loading = false
}
</script>

最朴素也最有效。模板里写 state.list 啰嗦但绝对可靠。

方案二:改用 ref 一把梭

vue 复制代码
<script setup>
const list = ref([])
const loading = ref(false)
const page = ref(1)
const total = ref(0)
// ref 解构出来直接就是响应式的,不存在丢失问题
</script>

ref 存储原始值时,通过 .value 读写本质上就是通过 getter/setter,解构出来的 list 仍然是 Ref 对象引用,响应性不会丢。

方案三:需要解构时用 toRefs

vue 复制代码
<script setup>
const state = reactive({
  list: [],
  loading: false,
  page: 1,
  total: 0
})

// ✅ toRefs 把每个属性变成 ref
const { list, loading, page, total } = toRefs(state)

// 现在可以愉快解构了
console.log(page.value)  // 1
loading.value = true     // ✅ 响应式更新
total.value = 100        // ✅ 触发渲染
</script>

原理toRefs 遍历 reactive 对象的每个 key,生成对应的 Ref(本质是 getter/setter),解构后拿到的每一个变量仍然是 Ref 对象引用,能做到"用值不等于断连"。

方案四:computed 读取不依赖解构

vue 复制代码
<script setup>
const state = reactive({ count: 0, double: 0 })

// ❌ 不要这样
// const { count } = state
// const double = computed(() => count * 2)  // 每次都是 0

// ✅ 这样
const double = computed(() => state.count * 2)

// ✅ 或包装成 ref
const count = computed(() => state.count)
</script>

要点总结

场景 推荐做法
少量字段,简单组件 直接 state.xxx,最稳
大量字段,频繁操作 ref 替代 reactive
必须在外部解构 reactive toRefs() 包一层
模板中解构(<template> 自动解构,不受影响(模板编译时自动包装)
嵌套深层的 reactive shallowReactive + 手动触发,或用 Pinia

一句话记住reactive 解构 = 抽走快照,toRefs 解构 = 保留通道。选哪个,看你想要的是值还是响应链路。


📌 冷知识 :Vue3 官方文档其实明确写了"reactive 的属性解构或展开为局部变量会丢失响应性",但每 10 个 Vue3 开发者里至少有 6 个第一次碰到时都踩了。你不是一个人 🫡

相关推荐
云水一下1 小时前
Vue.js从零到精通系列(八):项目实战——构建一个完整的电商后台管理系统
前端·javascript·vue.js
LAM LAB1 小时前
【Web】网页如何模拟移动端获取定位\定位模拟测试
开发语言·前端·javascript
yunceqing1 小时前
从Excel调度到TMS平台:物流软件开发避坑清单
大数据·前端·网络·人工智能·excel·推荐算法
IT_陈寒1 小时前
Redis主从切换把我坑惨了,这份血泪史你最好看看
前端·人工智能·后端
weixin_471383031 小时前
Taro-04-网络请求
前端·javascript·taro
Doker 多克1 小时前
Spring AI Alibaba—快速构建ReactAgent
java·开发语言·前端·ai编程
快乐的哈士奇1 小时前
【Next.js实战②】Excel 派送表动态解析:表头识别与 FIELD_ALIASES 映射
前端·javascript·excel
2401_840759762 小时前
2026年前端框架生态与AI开发新趋势
前端·人工智能·科技
i220818 Faiz Ul2 小时前
药店管理|基于springboot + vue药店管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·美食分享系统