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. 但 total 和 loading 是原始值 (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 个第一次碰到时都踩了。你不是一个人 🫡

相关推荐
lovingsoft8 分钟前
Chrome Lighthouse 详解
前端·chrome
Qlly18 分钟前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝22 分钟前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦23 分钟前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡44 分钟前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载1 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码
葡萄城技术团队1 小时前
用 SpreadJS 数据验证实现省市区三级联动
前端