4、深入响应式系统:Proxy 原理、ref 与 reactive 的异同、响应式丢失问题
Vue 3 的响应式系统,可以说是整个框架最核心的升级。说实话,我刚从 Vue 2 切过来时,第一反应是「这玩意儿到底怎么工作的?」。今天我们就把它彻底拆开,看看 Proxy 到底强在哪,ref 和 reactive 该怎么选,以及那些让人头疼的「响应式丢失」问题。
4.1 Proxy 原理:Vue 3 的底层魔法
Vue 2 用的是 Object.defineProperty,说白了就是给每个属性加个「监听器」。但有个硬伤——它没法检测新增属性和删除属性。你想想看,如果你动态给对象加个字段,Vue 2 根本不知道。
Vue 3 换成了 Proxy,这玩意儿直接代理整个对象。什么意思呢?就像你在对象外面包了一层「拦截器」,任何读写操作都得先经过它。
const target = { name: 'Vue3' }
const handler = {
get(target, key, receiver) {
console.log(`读取了 ${key}`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`设置了 ${key} = ${value}`)
return Reflect.set(target, key, value, receiver)
}
}
const proxy = new Proxy(target, handler)
proxy.name // 输出: 读取了 name
proxy.age = 18 // 输出: 设置了 age = 18
看到了吗?新增属性 age 也能被拦截到。这就是 Proxy 最大的优势——全量拦截。我在项目中遇到过这样一个场景:后端返回的数据结构里有个嵌套对象,Vue 2 里我得手动 Vue.set,Vue 3 直接赋值就行,省心太多了。
核心区别一句话总结:
- Vue 2 的 defineProperty 是「逐个属性监听」
- Vue 3 的 Proxy 是「整个对象代理」
4.2 ref 与 reactive 的异同
这两个 API 是 Vue 3 响应式系统的「双胞胎」,但用法完全不同。我刚开始也经常搞混,后来总结了一套自己的判断标准。
4.2.1 reactive:只能代理对象
reactive 接收一个对象,返回它的响应式代理。注意,它只接受对象类型(包括数组、Map、Set 等)。
import { reactive } from 'vue'
const state = reactive({ count: 0, list: [] })
state.count++ // 响应式更新
但有个坑——reactive 不能直接代理基本类型。你写 reactive(0) 会直接报错。为什么?因为 Proxy 只能代理对象,基本类型没法代理。
4.2.2 ref:全能选手
ref 就灵活多了。它内部其实也是用 reactive 实现的,但包装了一层 .value。说白了,ref 就是把任何值都包成一个对象,然后交给 reactive 处理。
import { ref } from 'vue'
const count = ref(0)
count.value++ // 注意要 .value
// 对象也可以
const user = ref({ name: '张三' })
user.value.name = '李四' // 也是响应式的
我个人习惯是:基本类型用 ref,对象类型用 reactive。但如果你在模板里用 ref,Vue 会自动解包 .value,所以模板里直接写 count 就行。
我的经验之谈:
如果你不确定用哪个,直接用 ref 准没错。ref 是 reactive 的超集,什么类型都能处理。而且 ref 在组合式函数里传递更方便,不会丢失响应式。
4.2.3 对比表格
| 特性 | ref | reactive |
|---|---|---|
| 支持类型 | 任意类型(基本类型 + 对象) | 仅对象类型 |
| 访问方式 | 需要 .value(模板自动解包) |
直接访问属性 |
| 解构安全性 | 安全(解构后仍是 ref) | 不安全(解构后丢失响应式) |
| 内部实现 | 基于 reactive 包装 | 基于 Proxy |
| 推荐场景 | 基本类型、需要传递的响应式值 | 组件内部复杂状态对象 |
4.3 响应式丢失问题
这是 Vue 3 新手最容易踩的坑。什么叫「响应式丢失」?就是你明明用了响应式 API,但修改数据后视图不更新。
4.3.1 解构导致丢失
最常见的情况——解构 reactive 对象。
const state = reactive({ count: 0, name: 'Vue' })
// ❌ 这样会丢失响应式
let { count, name } = state
count++ // 视图不会更新
// ✅ 正确做法:用 toRefs
import { toRefs } from 'vue'
let { count, name } = toRefs(state)
count.value++ // 视图更新
为什么会这样?因为解构出来的 count 只是一个普通变量,跟原来的响应式对象没关系了。toRefs 会把每个属性变成 ref,这样解构后仍然是响应式的。
4.3.2 赋值整个对象
另一个常见场景——直接给 reactive 对象赋新值。
const state = reactive({ list: [] })
// ❌ 这样会丢失响应式
state = { list: [1, 2, 3] } // 报错!不能重新赋值
// ✅ 正确做法:修改属性
state.list = [1, 2, 3]
// 或者用 Object.assign
Object.assign(state, { list: [1, 2, 3] })
我记得有一次,同事从接口拉数据后直接 state = res.data,结果页面死活不更新。排查了半天才发现是这个问题。嗯,这里要注意:reactive 对象的引用不能变,只能改里面的属性。
4.3.3 数组索引和长度
虽然 Proxy 解决了 Vue 2 中数组索引的问题,但有些操作还是要注意。
const arr = reactive([1, 2, 3])
// ✅ 直接修改索引没问题
arr[0] = 10
// ✅ 修改长度也没问题
arr.length = 2
// ❌ 但直接替换整个数组会丢失
arr = [4, 5, 6] // 错误!
避坑指南:
我曾经在写一个表格组件时,用 arr = newArray 来更新数据,结果表格不刷新。后来改成 arr.splice(0, arr.length, ...newArray) 才解决。记住:永远不要重新赋值 reactive 变量本身。
4.3.4 深层嵌套的陷阱
Proxy 是「懒代理」的——只有当你访问到某个属性时,它才会递归代理。这本身没问题,但如果你把嵌套对象赋值给另一个变量,可能会出问题。
const state = reactive({
user: { name: '张三', info: { age: 18 } }
})
// 这样没问题
const userInfo = state.user.info
userInfo.age = 20 // 视图更新
// 但如果你把 user 整个拿出来再修改
const user = state.user
user = { name: '李四', info: { age: 25 } } // ❌ 丢失响应式
说白了,只要你不破坏 reactive 对象的引用链,响应式就还在。一旦你「断开连接」,那就没戏了。
4.4 总结
响应式系统是 Vue 3 的基石。Proxy 让 Vue 3 能拦截所有操作,ref 和 reactive 各有适用场景,而响应式丢失问题——记住一句话:别解构 reactive,别重新赋值 reactive 变量。
如果你刚开始用 Vue 3,我建议你统一用 ref + toRefs 的组合。虽然多写几个 .value,但至少不会踩响应式丢失的坑。等你熟悉了,再根据场景灵活选择。
下一章我们会聊聊 Composition API 的实战技巧,到时候会用到今天讲的这些知识。嗯,先消化一下吧。