深入理解 Vue 3 的 ref 和 reactive:响应式底层差异与选择策略
深入理解 Vue 3 的 ref 和 reactive:响应式底层差异与选择策略
Vue 3 的响应式系统比 Vue 2 强了很多,但 ref 和 reactive 到底用哪个、什么时候用、为什么有时候响应式会"失效",这些问题如果不清楚,写出来的代码很可能踩坑。
我之前也模棱两可,后来花时间看了源码,理解了底层逻辑,现在写代码心里踏实很多。
先说结论
如果你只想知道怎么选:
- 基本类型(string、number、boolean)用
ref - 对象、数组用
reactive - 组合式函数(composable)的返回值用
ref(方便解构) - 深度响应式需求用
reactive
但知道怎么选不够,你还得知道为什么。
ref 和 reactive 的底层实现差异
reactive 的实现
reactive 底层用的是 Proxy。当你这样写时:
const state = reactive({ count: 0 })
Vue 3 实际做的是:
state = new Proxy({ count: 0 }, {
get(target, key, receiver) {
track(target, 'get', key) // 依赖收集
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, 'set', key) // 触发更新
return result
}
})
Proxy 可以拦截对象的所有操作(get、set、deleteProperty 等),所以对整个对象的任何修改都能被捕获。
ref 的实现
ref 底层其实是一个对象,包含一个 value 属性:
const count = ref(0)
// 展开大概是这个样子
const count = {
value: 0,
_isRef: true,
}
Vue 对 ref 的处理比较特殊:
- 当你在模板里访问
{{ count }}时,Vue 自动帮你解开.value - 当你在
reactive对象里放一个ref时,Vue 也会自动解开
所以 ref 本质上是为了解决基本类型的响应式问题——因为 Proxy 只能代理对象,不能代理基本类型。
响应式失效的典型场景
理解了底层,就能明白为什么会有这些常见问题了。
问题一:解构 reactive 对象后丢失响应式
const state = reactive({ count: 0, name: '张三' })
// 这样解构会丢失响应式
const { count, name } = state
console.log(count) // 0,但不再是响应式的
count++ // 视图不会更新!
为什么?因为 count 和 name 只是普通的常量,值已经被提取出来了,Proxy 的拦截对它们无效。
解决方案:用 toRefs:
const state = reactive({ count: 0, name: '张三' })
// 这样解构保持响应式
const { count, name } = toRefs(state)
// count 和 name 现在是 ref,修改时依然会触发更新
count.value++
问题二:reactive 数组直接赋值整个数组
const list = reactive([1, 2, 3])
// 这样赋值会丢失响应式
list = reactive([4, 5, 6]) // ERROR: Assignment to constant
// 正确做法
list.length = 0 // 清空
list.push(4, 5, 6) // 添加新元素
其实 reactive 数组你一般不会重新赋值整个数组,但如果是 reactive 的对象:
const state = reactive({ items: [1, 2, 3] })
// 整个替换 items 数组
state.items = [4, 5, 6] // 这是可以的!Proxy 捕获了 set 操作
问题三:reactive 嵌套对象内部新增属性
const state = reactive({ profile: { name: '张三' } })
// 新增一个属性
state.profile.age = 20 // age 不会是响应式的!
因为 Vue 3 的 reactive 只对已有的属性创建响应式追踪。新增的属性需要用 set 或者一开始就把所有属性定义好。
正确做法:
// 方案一:一开始就定义完整
const state = reactive({ profile: { name: '张三', age: undefined } })
// 方案二:使用 ref
const profile = ref({ name: '张三' })
profile.value.age = 20 // ref 的 value 是响应式的
// 方案三:使用 Vue 3.5+ 的浅响应式配合 deep
import { reactive, isRef } from 'vue'
const state = reactive({
profile: {
name: '张三',
// Vue 3.5+ 可以直接这样写
age: $ref(0) // 或者用 reactive 包裹
}
})
ref 和 reactive 的选择策略
什么时候用 ref
1. 基本类型必须用 ref
const count = ref(0)
const name = ref('')
const isLoading = ref(false)
2. 组合式函数的返回值应该用 ref
function useCounter() {
const count = ref(0)
function increment() {
count.value++
}
// 返回 ref,用户可以解构使用
return { count, increment }
}
// 调用方可以这样用
const { count, increment } = useCounter()
3. 需要控制响应式开启/关闭时用 ref
const shouldWatch = ref(true)
watchEffect(() => {
if (shouldWatch.value) {
console.log(state.count) // 只有 shouldWatch 为 true 时才收集依赖
}
})
什么时候用 reactive
1. 有一组相关的状态,用对象组织更清晰时
const formState = reactive({
username: '',
password: '',
rememberMe: false,
})
2. 需要对整个对象做响应式代理时
const tableConfig = reactive({
columns: ['name', 'status', 'actions'],
sortable: true,
filterable: true,
pageSize: 20,
})
ref 和 reactive 混用的场景
最常见的是在 reactive 对象里放 ref:
const state = reactive({
count: ref(0), // ref 会自动解开,但保持响应式
})
// 这样访问和修改
console.log(state.count) // 自动解开,直接是 0
state.count++ // 直接操作,不需要 .value
// 但注意,如果是嵌套的 ref,还是要 .value
const outer = ref({ inner: ref(0) })
console.log(outer.value.inner.value) // 必须用 .value 两次
性能方面的考虑
ref 的开销
每次访问 ref 的 .value 都有一点开销(虽然是纳秒级)。如果你有这样的场景:
// 在一个循环里频繁访问
for (let i = 0; i < 100000; i++) {
process(state.count) // 每次都要 .value
}
可以先用局部变量暂存:
const countValue = state.count
for (let i = 0; i < 100000; i++) {
process(countValue) // 更快
}
reactive 的深度代理开销
reactive 会对对象进行深度代理。如果你的对象树很深很大,初始化时会慢一些。另外,每次访问深层属性也会多走几层 Proxy 链。
对于超大型数据结构,可以考虑用 shallowReactive:
const state = shallowReactive({
// 只有第一层是响应式的
deep: {
nested: {
value: 1 // 这里不是响应式的
}
}
})
但用了 shallowReactive 就意味着深层修改不会触发更新,要根据业务场景选择。
实际项目中的最佳实践
综合以上分析,我在实际项目中这样用:
// 1. 基本类型用 ref
const isLoading = ref(false)
const currentId = ref<string | null>(null)
// 2. 相关联的状态用 reactive 对象
const filterState = reactive({
keyword: '',
status: '',
dateRange: [] as string[],
})
// 3. 表单场景用 reactive + ref 混合
const formState = reactive({
username: '',
password: '',
})
const { username, password } = toRefs(formState)
// 4. 组合式函数返回 ref
export function usePagination() {
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
function nextPage() {
page.value++
}
return {
page,
pageSize,
total,
nextPage,
}
}
// 5. 需要深度响应式的大型对象用 reactive
const globalState = reactive({
user: {
id: '',
name: '',
permissions: [] as string[],
},
settings: {
theme: 'light',
language: 'zh-CN',
},
})
总结
理解 ref 和 reactive 的底层差异,能帮你在写代码时做出更正确的选择:
- 基本类型用 ref,对象/数组用 reactive——这是最基本的原则
- reactive 对象解构要用
toRefs——否则丢失响应式 - reactive 只对已有属性创建响应式——新增属性要考虑
- 组合式函数返回 ref——方便调用方解构使用
- 超大型对象考虑
shallowReactive——减少初始化开销
写 Vue 3 代码时多思考响应式边界这些问题,上线后能少很多莫名其妙的 bug。
