当前位置: 首页 > news >正文

Vue3 响应式原理与 Composition API 实战踩坑:我被这些细节坑了3次后终于搞懂了

Vue3 响应式原理与 Composition API 实战踩坑:我被这些细节坑了3次后终于搞懂了

前言:我的踩坑血泪史

大家好,我是二白,一个刚入门 Vue3 不久的后端开发。之前一直用 Vue2,觉得响应式用起来很简单,直到项目升级到 Vue3 后,我被 Composition API 和响应式系统坑了无数次。

印象最深的是那次做一个后台管理系统,页面数据怎么都不更新,我改来改去改了3个小时,最后才发现是refreactive用混了。还有一次,用watch监听一个对象,改动属性后监听器怎么都不触发,浪费了一下午排查。

今天把这些踩坑经验整理出来,希望能帮到和我一样刚入坑 Vue3 的同学。文章会从最基础的响应式原理讲起,然后分享 5 个我在项目中真实遇到的坑,每个坑都会给出完整代码示例。

一、ref vs reactive:到底该用哪个?

问题描述

刚开始用 Vue3 时,我完全搞不清楚什么时候用ref,什么时候用reactive。为了方便,我总是随手用其中一个,结果经常遇到数据不更新或者控制台报警告的问题。

有一次我这样写:

import{ref,reactive}from'vue'// 随手用 refconstform=ref({name:'',email:''})// 修改数据functionupdateName(){form.name='张三'// 报错了!form.value 才是实际值}

原因分析

refreactive的核心区别在于:

  1. ref:接收原始值,返回一个响应式对象。访问和修改值需要通过.value
  2. reactive:接收对象,返回一个响应式代理,直接访问属性即可

Vue2 升级到 Vue3 后,为了解决响应式丢失问题,Vue3 使用了 Proxy,但ref是创建一个通过包含.value属性的对象来实现响应式的。

解决方案

记住一个简单规则:基本类型用 ref,对象/数组用 reactive

import{ref,reactive}from'vue'// 基本类型用 refconstcount=ref(0)constmessage=ref('Hello')// 对象/数组用 reactiveconstuser=reactive({name:'张三',age:25})constlist=reactive([])// 修改方式functionupdate(){count.value++// ref 需要 .valueuser.name='李四'// reactive 直接修改}

完整示例

<template> <div> <p>计数:{{ count }}</p> <p>用户:{{ user.name }} - {{ user.age }}</p> <button @click="increment">+1</button> <button @click="changeUser">换人</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定义了一个对象,后来想整个替换它,结果发现数据完全不更新。

conststate=reactive({list:[]})// 模拟接口返回新数据functionloadData(){state.list=[1,2,3]// 不生效!}

原因分析

reactive返回的是一个 Proxy 对象,它会追踪属性的读取和修改。但是,如果你把整个对象重新赋值,响应式就会断开

因为state.list = [1, 2, 3]是给state这个 Proxy 的list属性重新赋值,而不是修改原有数组,Vue3 可以检测到。但如果是这样:

conststate=reactive({list:[]})// 错误:整个替换 reactive 对象state=reactive({list:[1,2,3]})

这完全没有响应性,因为state本身被重新赋值了。

解决方案

如果你需要重置整个对象,有几种方式:

方式1:使用 ref 包装对象

import{ref}from'vue'conststate=ref({list:[]})functionloadData(){state.value={list:[1,2,3]}}

方式2:使用 Object.assign

conststate=reactive({list:[]})functionloadData(){Object.assign(state,{list:[1,2,3]})}

方式3:清空后重新赋值

conststate=reactive({list:[]})functionloadData(){state.list.length=0// 清空state.list.push(1,2,3)// 重新添加}

完整示例

<template> <div> <ul> <li v-for="item in state.list" :key="item">{{ item }}</li> </ul> <button @click="loadData">加载数据</button> <button @click="reset">重置</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 对象的一个属性,结果修改属性后监听器完全不触发。

constuser=reactive({name:'张三',info:{age:25}})watch(user.info,(newVal,oldVal)=>{console.log('info 变化了',newVal)},{deep:true})// 修改了但监听器没触发user.info.age=30

原因分析

问题在于user.info在监听时获取到的是一个普通对象,而不是响应式对象。Vue3 的watch第一个参数需要是:

  • 一个 ref
  • 一个响应式对象
  • 或者一个 getter 函数

直接传入user.info会导致监听失效。

解决方案

使用 getter 函数或者监听整个对象:

import{reactive,watch}from'vue'constuser=reactive({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 @click="user.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,但是它就是不更新,特别奇怪。

constcount=ref(0)constmultiplier=ref(2)consttotal=computed(()=>{returncount.value*multiplier.value})functionincrement(){count.value++console.log('total:',total.value)// 居然没变化?}

原因分析

这个问题通常是两个原因:

  1. computed 依赖的值不是响应式的:如果依赖的值不是 ref/reactive,computed 就不会更新
  2. 在 setup 中直接访问 computed 值:computed 返回的也是一个 ref,访问需要.value,但在模板中不需要

解决方案

确保依赖是响应式的,并正确访问 computed 值:

import{ref,computed}from'vue'constcount=ref(0)constmultiplier=ref(2)// computed 返回的也是 refconsttotal=computed(()=>{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 @click="count++">增加数量</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 对象的数据,结果发现数据是初始值,不是最新的。

constuser=reactive({name:'张三'})onMounted(()=>{console.log('user:',user.name)// 永远是'张三'})functionupdateName(){user.name='李四'}

原因分析

其实这个问题不是onMounted的问题,而是异步回调的问题。如果你是在onMounted的异步回调中访问响应式数据,要小心闭包陷阱。

constuser=reactive({name:'张三'})onMounted(()=>{// 这个回调会在组件挂载后执行// 但 user 是响应式的,这里应该能拿到最新值console.log('user:',user.name)})// 真正的坑:异步回调onMounted(()=>{setTimeout(()=>{// 这里能拿到最新值吗?能!console.log('3秒后:',user.name)},3000)})

实际上,上面的代码应该能正常工作。真正的问题是这样的:

// 错误示例:在 onMounted 中创建闭包onMounted(()=>{constname=user.name// 这里捕获了初始值// 3秒后打印的还是初始值setTimeout(()=>{console.log(name)// '张三',不是最新的},3000)})

解决方案

不要在onMounted中提前捕获响应式数据,要在回调中直接访问:

import{reactive,onMounted}from'vue'constuser=reactive({name:'张三'})// 正确:直接在回调中访问onMounted(()=>{console.log('挂载时:',user.name)setTimeout(()=>{console.log('3秒后:',user.name)// 能拿到最新值},3000)})// 正确:使用 watchimport{watch}from'vue'watch(()=>user.name,(newVal)=>{console.log('name 变化了:',newVal)})functionupdateName(){user.name='李四'}

完整示例

<template> <div> <p>用户名:{{ user.name }}</p> <button @click="user.name = '李四'">改名</button> <button @click="fetchUser">异步获取用户</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 个坑:

  1. ref vs reactive:基本类型用 ref,对象用 reactive
  2. reactive 重新赋值:用 ref 包装对象,或使用 Object.assign
  3. watch 监听:使用 getter 函数监听深层属性
  4. computed 访问:记得 computed 也是 ref,模板外需要.value
  5. onMounted 闭包:不要提前捕获值,要在回调中直接访问响应式对象

希望这些经验能帮你在 Vue3 的学习中少走弯路。如果觉得有用,点个赞再走~有问题也欢迎在评论区讨论!


我是二白,一个正在成长的全栈开发者。关注我,一起学习更多前端/vue3 开发经验!

http://www.jsqmd.com/news/437193/

相关文章:

  • 2026年3月定制异型电永磁吸盘厂家推荐,异型定制按需设计厂家 - 品牌鉴赏师
  • 2026年3月不锈钢法兰盘毛坯厂家推荐,不锈钢防腐防锈优质厂家 - 品牌鉴赏师
  • LCT详解
  • 本地部署开源数据可视化和协作工具 Redash 并实现外部访问
  • Flutter 三方库 flutterando_analysis 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、工业级的代码静态审计与工程质量守卫引擎
  • Flutter 三方库 sort_pubspec_dependencies 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、基于依赖项排序的工业级 pubspec.yaml 指导与工程审计引擎
  • Flutter 三方库 jaspr_content 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、基于 Jaspr 框架的工业级内容分发、由于博客系统与静态网站审计引擎
  • Flutter 三方库 meedu 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、基于反应式编程(Reactive)的工业级状态管理、依赖注入与全局响应式架构引擎
  • 国产脱氧机哪家好?优质品牌推荐及核心参数全解析 - 品牌推荐大师
  • Flutter 三方库 langchain_core 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、基于 LangChain 核心抽象的工业级大语言模型(LLM)应用编排与逻辑通信引擎
  • AI原生应用领域意图预测技术大解析
  • 2026年3月激光割法兰毛坯厂家推荐,激光切割精度高厂家 - 品牌鉴赏师
  • Flutter 三方库 string_extensions 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、基于 String 原生扩展的工业级文本审计与逻辑加工引擎
  • 2026年3月广告无纺布袋厂家推荐,广告推广logo定制厂家 - 品牌鉴赏师
  • GESP C++2024年12月四级考试编程题(第二题 字符排序)详细解析
  • 免费体验阿里小龙虾Copaw!比OpenClaw可简单多了
  • 2026年3月野餐收纳包厂家推荐,野餐露营大容量收纳厂家 - 品牌鉴赏师
  • 计算机大学生必看!计算机专业未来发展全景图(2025-2030)
  • Flutter 三方库 built_redux 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、基于不可变模型(Immutability)的工业级 Redux 状态审计与内存治理引擎
  • uvm仿真运行的核心机制
  • 2026年3月气体质量流量传感器厂家推荐,高性能与可靠性兼具的优质品牌 - 品牌鉴赏师
  • 2026年3月气体质量流量表厂家推荐,高性能与可靠性兼具的优质品牌 - 品牌鉴赏师
  • 2026年3月束口抽绳收纳袋厂家推荐,束口设计使用便捷优质厂家 - 品牌鉴赏师
  • SQLite3学习笔记7:prepare + bind(C API)
  • Flutter 三方库 formdator 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、基于装饰器模式(Decorator Pattern)的工业级表单字段校验与逻辑组合审计引擎
  • 计算机毕业设计springboot露营装备租赁系统 基于SpringBoot的户外露营装备共享租赁平台 基于SpringBoot的野营器材在线租借管理系统
  • 【MySQL数据库基础】(二)MySQL 数据库基础从入门到上手,一篇带你吃透核心知识点!
  • 2026七彩喜智慧养老解决方案:从“适老化“到“智老化“的范式转型
  • 深入浅出:扩散模型Classifier Guidance技术全景解读
  • 具身智能篇---CLIP (Contrastive Language-Image Pre-training)