Vue3 响应式原理:我被 ref 和 reactive 坑了3次后终于搞懂了
说实话,刚从 Vue2 转到 Vue3 的时候,我对 ref 和 reactive 这两个 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 } // 响应式断了!
当你用解构赋值时,count、name、email 变成了普通的常量,不再是对原始响应式对象的引用。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 回调中收到的
newVal和oldVal对于对象类型是同一个引用,需要小心
踩坑现场五:异步更新居然看不到?
问题描述
我想在数据更新后获取 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 复杂,但它也更强大、更灵活。记住这几个关键点:
- ref 是值包装 :修改值要用
.value,模板中自动解包 - reactive 是对象包装:不要解构,保持引用,数组用 ref 更安全
- 数组操作要小心:不要用索引直接赋值,用 push/splice 等方法
- computed 和 watch:computed 要 .value,watch 的源不需要
- 异步更新要 nextTick:DOM 更新是异步的
说白了,Vue3 的响应式就是一个原则:搞清楚你在操作的是值还是引用。
一句话总结:ref 盒子开,reactive 别拆,数组用 ref 不用 reactive,异步更新等 nextTick。
如果你也被 ref/reactive 坑过,欢迎在评论区分享你的踩坑经历大家一起避坑!觉得有帮助的点个赞支持下~
相关推荐: