Vue3 响应式原理:我被 ref 和 reactive 坑了3次后终于搞懂了

Vue3 响应式原理:我被 ref 和 reactive 坑了3次后终于搞懂了

说实话,刚从 Vue2 转到 Vue3 的时候,我对 refreactive 这两个 API 真是又爱又恨。爱的是它们让响应式更灵活了,恨的是稍不留神就掉进坑里,页面就是不更新。

我的踩坑血泪史 :记得那是一个周五下午,产品经理催着要改一个列表页的需求。我信心满满地用了 reactive 写了一个响应式表格,结果改数据时页面毫无反应。我排查了整整3个小时,换了无数种写法,最后才发现是解构赋值惹的祸------这个问题我居然在同一个项目里犯了3次!

今天把我的踩坑经历完整分享出来,保证你以后不再踩同样的坑。建议先收藏再看,避免以后找不到。


踩坑现场一:ref 居然拿不到值?

问题描述

那天下午,我信心满满地写了一个计数器组件:

javascript 复制代码
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    function add() {
      count = count + 1  // 报错!
    }
    
    return { count, add }
  }
}

点击按钮时直接报错:Uncaught TypeError: Assignment to constant variable

当时我的内心:这什么鬼?Vue2 里不都是直接写的吗?怎么到 Vue3 就报错了?

原因分析

这是一个非常常见的新手问题。ref 返回的是一个响应式引用对象(Ref 对象),不是原始值。你可以把它理解成一个"盒子",值就放在这个盒子里面。

  • count 是一个 ref 对象
  • 真正的值在 count.value 里面
  • 模板中 Vue 会自动拆开这个"盒子",所以不需要写 .value

这就是为什么直接 count = count + 1 会报错------你在尝试重新赋值一个常量。

解决方案

正确写法

javascript 复制代码
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    function add() {
      count.value++  // 使用 .value 访问值
    }
    
    return { count, add }
  }
}

模板中的自动解包

vue 复制代码
<template>
  <button @click="add">{{ count }}</button>
  <!-- 模板中 count 自动解包,不需要 count.value -->
</template>

⚠️ 注意事项

  • 记住:在 script 中用 .value,在 template 中不用
  • 如果你在 console.log(count),打印出来的是 { value: 0 } 对象,不是数字
  • 这不是 Vue3 的 bug,是设计如此

踩坑现场二:reactive 对象居然不响应?

问题描述

这是一个让我掉了3次头发的坑。我当时想用 reactive 创建一个用户状态对象:

javascript 复制代码
import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({
      count: 0,
      name: '小明',
      email: 'xiaoming@example.com'
    })
    
    function updateName() {
      state.name = '大明'
      console.log(state.name)  // 值确实变了!
    }
    
    return { state, updateName }
  }
}

诡异的事情发生了 :我在控制台打印 state.name,值确实变成了"大明",但模板就是不变!

原因分析

问题出在 解构赋值 上!让我还原我犯的错误:

javascript 复制代码
// 这是错误示范!
const { count, name, email } = state  // 这样做就失去了响应式!

return { count, name, email }  // 响应式断了!

当你用解构赋值时,countnameemail 变成了普通的常量,不再是对原始响应式对象的引用。Vue 监听不到它们的变化。

还有一种情况也会导致响应式丢失:

javascript 复制代码
// 错误:将 reactive 对象赋值给普通变量
const myState = state
return { myState }  // 这样也不行!

解决方案

方案一:直接返回 reactive 对象(推荐)

javascript 复制代码
import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({
      count: 0,
      name: '小明',
      email: 'xiaoming@example.com'
    })
    
    function updateName() {
      state.name = '大明'
    }
    
    // 保持对象引用,不要解构
    return { state, updateName }
  }
}

方案二 :使用 toRefs 转换

javascript 复制代码
import { reactive, toRefs } from 'vue'

export default {
  setup() {
    const state = reactive({
      count: 0,
      name: '小明',
      email: 'xiaoming@example.com'
    })
    
    // toRefs 把 reactive 对象的每个属性转成 ref
    // 这样解构后仍然是响应式的
    const { count, name, email } = toRefs(state)
    
    function updateName() {
      name.value = '大明'  // 注意:这里要用 .value
    }
    
    return { count, name, email, updateName }
  }
}

⚠️ 注意事项

  • 永远不要对 reactive 对象进行解构 ,除非用 toRefs
  • 如果你在函数间传递响应式对象,确保始终传递同一个引用
  • 警惕多层解构:const { user: { name } } = state 也会断开响应式

踩坑现场三:数组居然没响应?

问题描述

我想用 reactive 存一个待办事项列表:

javascript 复制代码
import { reactive } from 'vue'

export default {
  setup() {
    const todoList = reactive([])
    
    function addItem(item) {
      todoList.push({
        id: Date.now(),
        title: item,
        done: false
      })
      // 打印看是有数据的
      console.log(todoList)
    }
    
    return { todoList, addItem }
  }
}

坑爹啊! 不管我怎么 push,页面就是不动!但控制台明明能打印出数据。

原因分析

这其实不是 reactive 本身的问题,而是 Vue 响应式系统的触发机制问题。

在 Vue3 中,某些数组操作确实不会自动触发更新:

  • ❌ 直接赋值:todoList[0] = '新值'
  • ❌ 修改长度:todoList.length = 0
  • ❌ 使用索引:todoList[0].done = true

虽然 push 在大多数情况下应该能触发更新,但有时候会因为引用问题导致不响应。

解决方案

方案一:使用 ref 包装数组(强烈推荐)

javascript 复制代码
import { ref } from 'vue'

export default {
  setup() {
    const todoList = ref([])
    
    function addItem(item) {
      todoList.value.push({
        id: Date.now(),
        title: item,
        done: false
      })
    }
    
    return { todoList, addItem }
  }
}

方案二:强制触发更新

javascript 复制代码
function addItem(item) {
  todoList.push(newItem)
  // 强制触发更新(不推荐,但有效)
  todoList = [...todoList]
}

方案三:使用 Vue 提供的响应式数组方法

Vue3 已经修复了大部分数组响应式问题,确保使用以下方法:

javascript 复制代码
// 这些方法都会触发响应式更新
todoList.push(item)
todoList.pop()
todoList.splice(index, 1)
todoList.sort()
todoList.reverse()

// 或者创建新数组
todoList = todoList.filter(...)
todoList = todoList.map(...)

⚠️ 注意事项

  • 强烈建议用 ref([]) 而不是 reactive([]) 来存储数组
  • 如果必须用 reactive([]),记住不要用索引直接赋值
  • 警惕数组的"假修改":arr.length-- 这种操作不会触发更新

踩坑现场四:computed 和 watch 居然没生效?

问题描述

我写了 computed 和 watch,但它们好像没起作用:

javascript 复制代码
import { ref, computed, watch } from 'vue'

export default {
  setup() {
    const firstName = ref('张')
    const lastName = ref('三')
    
    // computed
    const fullName = computed(() => {
      return firstName + lastName  // 少了 .value!
    })
    
    // watch
    watch(firstName, (newVal) => {
      console.log('名字变了', newVal)
    })
    
    function changeName() {
      firstName = '李'  // 这样不会触发更新!
    }
    
    return { firstName, lastName, fullName, changeName }
  }
}

原因分析

又是一个忘记 .value 的问题!

  • computed 返回的是 Ref 对象,需要 .value 访问
  • watch 监听的是 Ref 对象,但回调函数中不需要
  • 修改 ref 的值必须用 .value = 或者在模板中通过方法修改

解决方案

javascript 复制代码
import { ref, computed, watch } from 'vue'

export default {
  setup() {
    const firstName = ref('张')
    const lastName = ref('三')
    
    // computed 需要 .value 访问
    const fullName = computed(() => {
      return firstName.value + lastName.value
    })
    
    // watch 的第一个参数是 ref,不需要 .value
    watch(firstName, (newVal, oldVal) => {
      console.log('名字变了', newVal, oldVal)
    })
    
    // 监听多个源,用数组
    watch([firstName, lastName], ([newFirst, newLast]) => {
      console.log('名字全改了', newFirst, newLast)
    })
    
    function changeName() {
      firstName.value = '李'  // 必须用 .value
    }
    
    return { firstName, lastName, fullName, changeName }
  }
}

⚠️ 注意事项

  • computed 是只读的:不要试图修改 computed 的值,它由依赖自动计算
  • watch 默认不监听深层变化 :如果监听对象,需要加 { deep: true }
  • watch 回调中收到的 newValoldVal 对于对象类型是同一个引用,需要小心

踩坑现场五:异步更新居然看不到?

问题描述

我想在数据更新后获取 DOM 元素做动画:

javascript 复制代码
import { ref } from 'vue'

export default {
  setup() {
    const show = ref(false)
    
    function toggle() {
      show.value = true
      // 这里获取元素,应该显示了吧?
      const el = document.querySelector('.box')
      console.log(el)  // 居然是 null!
    }
    
    return { show, toggle }
  }
}

原因分析

Vue 的响应式更新是异步的!当你修改 ref 的值时,Vue 不会立即更新 DOM,而是等到下一个"tick"才更新。

所以在修改值的同一同步代码块中,DOM 还没有更新。

解决方案

方案一 :使用 nextTick

javascript 复制代码
import { ref, nextTick } from 'vue'

export default {
  setup() {
    const show = ref(false)
    
    async function toggle() {
      show.value = true
      
      // 等待 DOM 更新
      await nextTick()
      
      const el = document.querySelector('.box')
      console.log(el)  // 现在能获取到了!
    }
    
    return { show, toggle }
  }
}

方案二:使用 watch 监听变化

javascript 复制代码
import { ref, watch } from 'vue'

export default {
  setup() {
    const show = ref(false)
    
    watch(show, async (newVal) => {
      if (newVal) {
        await nextTick()
        const el = document.querySelector('.box')
        // 做你想做的事
      }
    })
    
    function toggle() {
      show.value = !show.value
    }
    
    return { show, toggle }
  }
}

⚠️ 注意事项

  • Vue 更新是异步的:理解这一点能避免很多坑
  • 所有依赖 DOM 状态的代码都应该放在 nextTick
  • await nextTick() 是 Vue3 的写法,Vue2 用 this.$nextTick()

写在最后

Vue3 的响应式系统确实比 Vue2 复杂,但它也更强大、更灵活。记住这几个关键点:

  1. ref 是值包装 :修改值要用 .value,模板中自动解包
  2. reactive 是对象包装:不要解构,保持引用,数组用 ref 更安全
  3. 数组操作要小心:不要用索引直接赋值,用 push/splice 等方法
  4. computed 和 watch:computed 要 .value,watch 的源不需要
  5. 异步更新要 nextTick:DOM 更新是异步的

说白了,Vue3 的响应式就是一个原则:搞清楚你在操作的是值还是引用

一句话总结:ref 盒子开,reactive 别拆,数组用 ref 不用 reactive,异步更新等 nextTick。

如果你也被 ref/reactive 坑过,欢迎在评论区分享你的踩坑经历大家一起避坑!觉得有帮助的点个赞支持下~


相关推荐:

相关推荐
大鱼前端1 小时前
Veaury:让Vue和React组件在同一应用中共存的神器
前端·vue.js·react.js
五月君_1 小时前
继 React、Vue 之后,Three.js 也有 Skills 了!AI 写 3D 终于不“晕”了
javascript·vue.js·人工智能·react.js·3d
scan7241 小时前
大模型只是知道要调用工具,本身不
前端·javascript·html
摇滚侠2 小时前
01 基础语法 JavaScript 入门到精通全套教程
开发语言·javascript·ecmascript
云水一下2 小时前
CSS3从零基础到精通(一):前世今生与基础入门
前端·css3
顾凌陵2 小时前
CSRF&SSRF漏洞攻击的溯源分析与实战
前端·csrf
用户6919026813392 小时前
JS 初了解:从“网页玩具”到企业级语言的进化
javascript
月月大王的3D日记2 小时前
Three.js 材质篇(中):从兰伯特到PBR,一篇文章看懂五种光照材质
前端·javascript