Vue3 响应式原理与 Composition API 实战踩坑我被这些细节坑了3次后终于搞懂了前言我的踩坑血泪史大家好我是二白一个刚入门 Vue3 不久的后端开发。之前一直用 Vue2觉得响应式用起来很简单直到项目升级到 Vue3 后我被 Composition API 和响应式系统坑了无数次。印象最深的是那次做一个后台管理系统页面数据怎么都不更新我改来改去改了3个小时最后才发现是ref和reactive用混了。还有一次用watch监听一个对象改动属性后监听器怎么都不触发浪费了一下午排查。今天把这些踩坑经验整理出来希望能帮到和我一样刚入坑 Vue3 的同学。文章会从最基础的响应式原理讲起然后分享 5 个我在项目中真实遇到的坑每个坑都会给出完整代码示例。一、ref vs reactive到底该用哪个问题描述刚开始用 Vue3 时我完全搞不清楚什么时候用ref什么时候用reactive。为了方便我总是随手用其中一个结果经常遇到数据不更新或者控制台报警告的问题。有一次我这样写import{ref,reactive}fromvue// 随手用 refconstformref({name:,email:})// 修改数据functionupdateName(){form.name张三// 报错了form.value 才是实际值}原因分析ref和reactive的核心区别在于ref接收原始值返回一个响应式对象。访问和修改值需要通过.valuereactive接收对象返回一个响应式代理直接访问属性即可Vue2 升级到 Vue3 后为了解决响应式丢失问题Vue3 使用了 Proxy但ref是创建一个通过包含.value属性的对象来实现响应式的。解决方案记住一个简单规则基本类型用 ref对象/数组用 reactiveimport{ref,reactive}fromvue// 基本类型用 refconstcountref(0)constmessageref(Hello)// 对象/数组用 reactiveconstuserreactive({name:张三,age:25})constlistreactive([])// 修改方式functionupdate(){count.value// ref 需要 .valueuser.name李四// reactive 直接修改}完整示例template div p计数{{ count }}/p p用户{{ user.name }} - {{ user.age }}/p button clickincrement1/button button clickchangeUser换人/button /div /template script setup import { ref, reactive } from vue // 基本类型用 ref const count ref(0) // 对象用 reactive const user reactive({ name: 张三, age: 25 }) function increment() { count.value } function changeUser() { user.name 李四 user.age 30 } /script二、reactive 对象重新赋值失效问题描述这个问题坑了我很久。我用reactive定义了一个对象后来想整个替换它结果发现数据完全不更新。conststatereactive({list:[]})// 模拟接口返回新数据functionloadData(){state.list[1,2,3]// 不生效}原因分析reactive返回的是一个 Proxy 对象它会追踪属性的读取和修改。但是如果你把整个对象重新赋值响应式就会断开。因为state.list [1, 2, 3]是给state这个 Proxy 的list属性重新赋值而不是修改原有数组Vue3 可以检测到。但如果是这样conststatereactive({list:[]})// 错误整个替换 reactive 对象statereactive({list:[1,2,3]})这完全没有响应性因为state本身被重新赋值了。解决方案如果你需要重置整个对象有几种方式方式1使用 ref 包装对象import{ref}fromvueconststateref({list:[]})functionloadData(){state.value{list:[1,2,3]}}方式2使用 Object.assignconststatereactive({list:[]})functionloadData(){Object.assign(state,{list:[1,2,3]})}方式3清空后重新赋值conststatereactive({list:[]})functionloadData(){state.list.length0// 清空state.list.push(1,2,3)// 重新添加}完整示例template div ul li v-foritem in state.list :keyitem{{ item }}/li /ul button clickloadData加载数据/button button clickreset重置/button /div /template script setup import { reactive, ref } from vue // 推荐用 ref 包装整个对象 const state ref({ list: [], loading: false }) function loadData() { state.value.loading true // 模拟接口 setTimeout(() { state.value { list: [1, 2, 3, 4, 5], loading: false } }, 1000) } function reset() { state.value { list: [], loading: false } } /script三、watch 监听不到对象属性变化问题描述这是我踩过最坑的一个问题。我用watch监听一个 reactive 对象的一个属性结果修改属性后监听器完全不触发。constuserreactive({name:张三,info:{age:25}})watch(user.info,(newVal,oldVal){console.log(info 变化了,newVal)},{deep:true})// 修改了但监听器没触发user.info.age30原因分析问题在于user.info在监听时获取到的是一个普通对象而不是响应式对象。Vue3 的watch第一个参数需要是一个 ref一个响应式对象或者一个 getter 函数直接传入user.info会导致监听失效。解决方案使用 getter 函数或者监听整个对象import{reactive,watch}fromvueconstuserreactive({name:张三,info:{age:25}})// 方式1使用 getter 函数watch(()user.info,(newVal,oldVal){console.log(info 变化了,newVal)},{deep:true})// 方式2监听整个对象watch(user,(newVal,oldVal){console.log(user 变化了,newVal)},{deep:true})// 方式3监听单个属性watch(()user.info.age,(newVal,oldVal){console.log(age 变化了,newVal)})完整示例template div p姓名{{ user.name }}/p p年龄{{ user.info.age }}/p button clickuser.info.age年龄1/button /div /template script setup import { reactive, watch } from vue const user reactive({ name: 张三, info: { age: 25 } }) // 监听深层属性 - 使用 getter watch(() user.info.age, (newVal, oldVal) { console.log(年龄从 ${oldVal} 变为 ${newVal}) }) // 监听整个 info 对象 - 使用 getter deep watch(() user.info, (newVal, oldVal) { console.log(info 对象变化了, newVal) }, { deep: true }) // 监听整个 user watch(user, (newVal, oldVal) { console.log(user 变化了, newVal) }, { deep: true }) /script四、computed 计算结果不更新问题描述有时候我定义了 computed但是它就是不更新特别奇怪。constcountref(0)constmultiplierref(2)consttotalcomputed((){returncount.value*multiplier.value})functionincrement(){count.valueconsole.log(total:,total.value)// 居然没变化}原因分析这个问题通常是两个原因computed 依赖的值不是响应式的如果依赖的值不是 ref/reactivecomputed 就不会更新在 setup 中直接访问 computed 值computed 返回的也是一个 ref访问需要.value但在模板中不需要解决方案确保依赖是响应式的并正确访问 computed 值import{ref,computed}fromvueconstcountref(0)constmultiplierref(2)// computed 返回的也是 refconsttotalcomputed((){console.log(计算了)returncount.value*multiplier.value})functionincrement(){count.value// 访问 computed 值需要 .valueconsole.log(total:,total.value)}// 在模板中不需要 .value// {{ total }}完整示例template div p数量{{ count }}/p p单价{{ price }}/p p总价{{ total }}/p !-- 模板中直接用不需要 .value -- p折扣价{{ discountedPrice }}/p button clickcount增加数量/button /div /template script setup import { ref, computed } from vue const count ref(0) const price ref(100) const discount ref(0.8) // 计算属性 - 依赖响应式数据 const total computed(() { return count.value * price.value }) // 带缓存的计算属性 const discountedPrice computed(() { return total.value * discount.value }) // 也可以使用 getter/setter const doubleCount computed({ get: () count.value * 2, set: (val) { count.value val / 2 } }) function doubleIt() { doubleCount.value 100 // 会触发 set更新 count } /script五、onMounted 中拿不到最新的响应式数据问题描述这个问题很隐蔽。我在onMounted里面调用了一个 reactive 对象的数据结果发现数据是初始值不是最新的。constuserreactive({name:张三})onMounted((){console.log(user:,user.name)// 永远是张三})functionupdateName(){user.name李四}原因分析其实这个问题不是onMounted的问题而是异步回调的问题。如果你是在onMounted的异步回调中访问响应式数据要小心闭包陷阱。constuserreactive({name:张三})onMounted((){// 这个回调会在组件挂载后执行// 但 user 是响应式的这里应该能拿到最新值console.log(user:,user.name)})// 真正的坑异步回调onMounted((){setTimeout((){// 这里能拿到最新值吗能console.log(3秒后:,user.name)},3000)})实际上上面的代码应该能正常工作。真正的问题是这样的// 错误示例在 onMounted 中创建闭包onMounted((){constnameuser.name// 这里捕获了初始值// 3秒后打印的还是初始值setTimeout((){console.log(name)// 张三不是最新的},3000)})解决方案不要在onMounted中提前捕获响应式数据要在回调中直接访问import{reactive,onMounted}fromvueconstuserreactive({name:张三})// 正确直接在回调中访问onMounted((){console.log(挂载时:,user.name)setTimeout((){console.log(3秒后:,user.name)// 能拿到最新值},3000)})// 正确使用 watchimport{watch}fromvuewatch(()user.name,(newVal){console.log(name 变化了:,newVal)})functionupdateName(){user.name李四}完整示例template div p用户名{{ user.name }}/p button clickuser.name 李四改名/button button clickfetchUser异步获取用户/button /div /template script setup import { reactive, onMounted, watch } from vue const user reactive({ name: 张三, loading: false }) // 正确在 onMounted 中直接访问响应式对象 onMounted(() { console.log(挂载时:, user.name) // 异步回调中直接访问可以拿到最新值 setTimeout(() { console.log(异步回调:, user.name) }, 2000) }) // 使用 watch 监听变化 watch(() user.name, (newVal, oldVal) { console.log(name 从 ${oldVal} 变为 ${newVal}) }) // 模拟异步操作 function fetchUser() { user.loading true setTimeout(() { user.name 从接口获取的数据 user.loading false }, 1000) } /script总结Vue3 的 Composition API 确实比 Vue2 的 Options API 更灵活但也多了很多需要注意的细节。回顾一下这篇文章提到的 5 个坑ref vs reactive基本类型用 ref对象用 reactivereactive 重新赋值用 ref 包装对象或使用 Object.assignwatch 监听使用 getter 函数监听深层属性computed 访问记得 computed 也是 ref模板外需要.valueonMounted 闭包不要提前捕获值要在回调中直接访问响应式对象希望这些经验能帮你在 Vue3 的学习中少走弯路。如果觉得有用点个赞再走有问题也欢迎在评论区讨论我是二白一个正在成长的全栈开发者。关注我一起学习更多前端/vue3 开发经验