Vue3里的ref和reactive到底有什么区别?新手别再乱用了!
刚开始接触Vue3的小伙伴,肯定会对Composition API里的两个响应式核心工具感到困惑:什么时候该用ref?什么时候该用reactive?看起来它们都能让数据变得“会动”,改了页面就跟着变,但用起来总有小麻烦——比如用ref包装对象之后要.value,忘了就报错;用reactive之后不能直接替换整个对象,不然响应式就丢了,今天咱们就掰开揉碎了讲,从底层原理、适用场景、常见坑点这几个维度,把这俩工具说透,看完保证你不会再踩雷。
为什么Vue3要搞两个响应式工具?
聊区别之前,得先搞懂Vue2的问题,以及Vue3的解决思路——只有知道背景,才能明白为什么不干脆只留一个。
Vue2用的是Object.defineProperty,这个API的缺点很明显:只能劫持对象的已有属性,如果后来给对象加了新属性,或者删了旧属性,页面根本不会更新;而且对数组的支持也不好,只能通过重写push、pop这些方法来实现响应式,直接用索引改数组(比如arr[0] = 'new')或者改数组长度(arr.length = 0),也是无效的。
Vue3就换成了Proxy,这个API能劫持整个对象(包括数组、Map、Set这些引用类型,还有后来新增/删除的属性),看起来完美解决了Vue2的问题对吧?那为什么还要搞ref?
因为Proxy只能劫持引用类型,对原始类型(比如字符串、数字、布尔值、null、undefined、Symbol)根本没用——原始类型是按值传递的,没有“对象”这个载体,Proxy找不到可以绑定的东西,那怎么办呢?Vue3就想到了“包装”:把原始类型装在一个普通的JavaScript对象里,这个对象有一个固定的属性叫.value,存的就是原始值,然后再用Proxy去劫持这个包装对象,哦,对了,这个包装对象就是ref。
那引用类型能不能用ref?当然能,Vue3内部会判断:如果给ref传的是引用类型,它会自动调用reactive去处理.value里的内容,所以其实ref包装的引用类型,底层还是Proxy。
从数据类型上直观区分
知道了背景,第一个直观的区别就出来了:
- ref:可以处理所有数据类型,原始类型、引用类型通吃;
- reactive:只能处理引用类型(对象、数组、Map、Set这些),原始类型传进去直接白给,不会有响应式。
举个简单的例子,比如我们要做一个计数器,存数字:
// 用ref没问题
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++ // 页面会跟着加1
// 用reactive试试?
import { reactive } from 'vue'
const count = reactive(0)
console.log(count) // 直接输出0,不是响应式对象
count++ // 页面完全没反应
再比如存一个用户信息对象:
// 用reactive没问题
import { reactive } from 'vue'
const user = reactive({
name: '张三',
age: 18
})
console.log(user.name) // 直接读,不用.value
user.age++ // 页面会跟着变
user.gender = '男' // 新增属性也有响应式
delete user.name // 删除属性也有响应式
// 用ref也能做
import { ref } from 'vue'
const user = ref({
name: '张三',
age: 18
})
console.log(user.value.name) // 这里要加.value
user.value.age++ // 加1
// 新增和删除属性也没问题,因为.value里的对象被reactive处理过了
user.value.gender = '男'
delete user.value.name
从使用方式上区分
除了数据类型,使用方式上的差异也很大,最典型的就是“.value的魔咒”,还有能不能直接替换整个数据。
要不要加.value?
这是新手最容易忘的点,也是最容易区分的地方:
- ref:在JavaScript代码里(包括setup函数、组合式函数、computed、watch这些) 必须加.value才能读取或修改值;但在模板(template)里 不需要,Vue3会自动解包。
- reactive:不管是在JavaScript代码里还是模板里,都不需要加.value,直接读写属性就行。
不过这里有个小例外:如果ref被放在一个reactive对象的顶层属性里,在JavaScript代码里也会自动解包,不用加.value。
import { ref, reactive } from 'vue'
const count = ref(0)
const state = reactive({
count // 顶层属性
})
console.log(state.count) // 0,不用加.value
state.count++ // 页面会加1
console.log(count.value) // 1,同步更新的
但如果ref被放在reactive对象的深层属性里,就不会自动解包了:
const state = reactive({
nested: {
count // 深层属性
}
})
console.log(state.nested.count.value) // 这里必须加.value
能不能直接替换整个数据?
这个坑很多老手也会踩,特别是从Vue2过渡来的,习惯了直接给data里的对象赋值,结果到Vue3用reactive就炸了。
- ref:可以直接替换整个数据,因为ref的响应式是绑定在包装对象上的,不是绑定在内部的.value上的,不管你把.value换成原始类型还是引用类型,包装对象的Proxy都还在,所以响应式不会丢。
- reactive:不能直接替换整个对象,因为reactive的响应式是绑定在传入的那个原始引用对象上的,如果直接把整个变量换成新对象,原来的Proxy就断了,新对象也没有Proxy,响应式就没了。
还是举用户信息的例子:
// 用ref直接替换没问题
const user = ref({
name: '张三',
age: 18
})
// 直接换整个新对象
user.value = {
name: '李四',
age: 20,
gender: '女'
}
console.log(user.value.name) // 李四,页面会更新
// 用reactive直接替换就凉了
const user = reactive({
name: '张三',
age: 18
})
// 直接换整个新对象
user = {
name: '李四',
age: 20,
gender: '女'
}
console.log(user.name) // 李四,但页面**不会更新**,因为原来的Proxy已经没了
那如果用reactive的时候必须更新整个对象怎么办?有两个办法: 第一个是用Object.assign把新对象的属性合并到原来的reactive对象里:
const user = reactive({
name: '张三',
age: 18
})
const newUser = {
name: '李四',
age: 20,
gender: '女'
}
// 合并属性,原来的Proxy还在
Object.assign(user, newUser)
console.log(user.name) // 李四,页面会更新
第二个是把reactive对象放在一个ref里,这样就可以直接替换了(其实这个时候底层还是ref的包装对象在起作用):
const user = ref(reactive({
name: '张三',
age: 18
}))
// 直接换整个新对象,这里新对象最好也用reactive处理一下,或者Vue3内部会自动处理?
// 对,之前说过,给ref传引用类型会自动调reactive,所以直接传普通对象也行
user.value = {
name: '李四',
age: 20,
gender: '女'
}
console.log(user.value.name) // 李四,页面会更新
从底层原理上区分(进阶版,想深入理解的看)
前面已经提了一点底层,但没说太细,这里展开讲,能帮你更透彻地理解为什么会有这些使用上的差异。
首先看Vue3的响应式系统核心,其实有两个独立的模块:
- 响应式数据创建模块:就是ref和reactive,负责把普通数据变成响应式的;
- 依赖收集和触发更新模块:负责收集哪些地方用到了响应式数据,当数据变化时通知这些地方更新。
先讲reactive的底层: reactive接收一个引用类型的参数,然后用Proxy创建一个代理对象,代理对象的getter会做依赖收集(比如如果在模板里用到了user.name,就会把模板的渲染函数加入到user.name的依赖列表里),setter会做触发更新(比如修改user.name的时候,就会遍历依赖列表,通知所有依赖的地方更新),reactive还会对Map、Set这些特殊的引用类型做单独的处理,重写它们的get、set、add、delete等方法,确保这些操作也能触发响应式,还有一个点,reactive是“深层响应式”的,也就是如果传入的对象有深层嵌套,Vue3会递归地用Proxy处理每一层的对象,所以不管你改多深的属性,都能触发更新,不过Vue3.3之后加了shallowReactive,浅层响应式”,只有第一层属性有响应式,深层的没有,这个后面讲常见工具对比的时候会提。
再讲ref的底层: ref接收一个任意类型的参数,然后返回一个RefImpl类的实例(RefImpl是Vue3内部定义的一个类,不是公开的API,但原理很简单),RefImpl类有两个属性:一个是_value,存的是传入的原始值;另一个是__v_isRef,这是一个内部的标识,用来告诉Vue3这个对象是ref,这样在模板里或者在reactive的顶层属性里才能自动解包,RefImpl类的getter会做依赖收集,setter会做触发更新,如果传入的参数是引用类型,setter里会自动调用reactive把_value变成响应式的;如果后来把_value换成了新的引用类型,setter也会再次调用reactive处理,所以ref不管存什么类型,不管怎么换_value,都能保持响应式。
哦,对了,还有一个细节:ref是“浅响应式”吗?不,不对——如果ref存的是原始类型,那本来就没有深层的东西;如果存的是引用类型,因为底层会调用reactive,所以默认也是“深层响应式”的,如果想要ref的引用类型是“浅层响应式”的,可以用shallowRef,后面也会提。
从适用场景上区分(最实用的部分,看完就能直接用)
前面讲了那么多原理和差异,最终目的还是为了知道什么时候用哪个,这里给大家总结几个通用的适用场景,照着用基本不会错。
什么时候用ref?
- 存储原始类型数据:比如数字、字符串、布尔值,这个是ref的专属场景,reactive根本处理不了;
- 需要直接替换整个数据:比如从接口获取到新的用户列表,要把旧的列表完全替换掉,这个时候用ref更方便,不用搞Object.assign;
- 组合式函数里返回数据:很多组合式函数(比如自定义的useCounter、useFetch)返回的都是ref,这样在使用的时候,不管是把它解构出来,还是放在reactive里,都很灵活;
- 存储DOM元素或组件实例:Vue3的template ref(就是用ref绑定DOM或者组件)默认返回的就是ref,
<template> <input ref="inputRef" /> </template>
什么时候用reactive?
- 存储一组相关的对象类型数据:比如用户信息、表单数据、列表的筛选条件,这些数据都是相关的,放在一个reactive对象里,读写属性的时候不用加.value,代码更简洁;
- 不需要直接替换整个数据:如果只是修改对象的属性、新增或删除属性,或者修改数组的元素、调用数组的方法,用reactive就很合适;
- 需要批量导出多个相关数据:比如在setup函数里,如果有很多相关的响应式数据,可以把它们放在一个reactive对象里,然后用toRefs把整个对象转换成ref的集合再导出,这样在组件外使用的时候可以解构,而且不会丢失响应式(这个后面讲toRef和toRefs的时候会提)。
别搞混!ref/reactive和它们的兄弟工具
除了ref和reactive,Vue3还有几个和它们相关的响应式工具,很多新手也会搞混,这里简单对比一下,帮你理清思路。
shallowRef vs ref
- shallowRef:只有整个.value的变化会触发响应式,.value内部的属性变化不会触发;
- ref:如果存的是引用类型,.value内部的属性变化也会触发响应式(因为底层用了reactive)。
适用场景:比如存一个很大的对象,只需要替换整个对象,不需要修改内部属性,用shallowRef可以提高性能,因为不需要递归地做深层响应式。
shallowReactive vs reactive
- shallowReactive:只有第一层属性的变化会触发响应式,深层属性的变化不会;
- reactive:深层属性的变化也会触发响应式。
适用场景:和shallowRef类似,存很大的对象,只需要修改第一层属性。
toRef vs toRefs
这两个工具都是用来把reactive对象的属性转换成ref的,主要是为了解决解构reactive对象会丢失响应式的问题。
const user = reactive({
name: '张三',
age: 18
})
// 直接解构,name和age都是普通变量,没有响应式
const { name, age } = user
name = '李四' // 页面不会更新
console.log(user.name) // 还是张三
这个时候就要用toRefs:
const user = reactive({
name: '张三',
age: 18
})
// 用toRefs转换成ref的集合,再解构,name和age都是ref,有响应式
const { name, age } = toRefs(user)
name.value = '李四' // 页面会更新
console.log(user.name) // 李四,同步更新的
那toRef是干嘛的?toRef是用来把reactive对象的单个属性转换成ref的,
const user = reactive({
name: '张三',
age: 18
})
// 只把name转换成ref
const name = toRef(user, 'name')
name.value = '李四' // 页面会更新
console.log(user.name) // 李四
还有一个区别:如果reactive对象的属性本来不存在,toRef会创建一个ref,但是修改这个ref的.value不会同步到原对象上;而toRefs只会转换原对象已有的属性。
新手最容易踩的5个坑
最后给大家列一下新手用ref和reactive最容易踩的5个坑,提前避坑:
- 用ref在JavaScript代码里忘了加.value:这个是最常见的,报错信息一般是“Cannot read properties of undefined (reading 'xxx')”或者“xxx is not a function”,解决办法就是养成习惯,在JavaScript代码里只要看到ref的变量,就先想想有没有加.value;
- 用reactive直接替换整个对象:前面讲过,解决办法是用Object.assign或者把reactive放在ref里;
- 直接解构reactive对象,丢失响应式:解决办法是用toRefs转换之后再解构;
- 给ref存的引用类型直接赋值内部属性时,忘了其实底层是reactive(这个其实不算坑,就是知道原理之后更安心):比如给user.value.name赋值,不用担心没有响应式,因为Vue3内部已经处理了;
- 在template里用ref的时候加了.value:虽然不会报错,但Vue3会自动解包,加了也是多余的,而且代码看起来乱,解决办法就是template里直接用ref的变量名。
一句话记住区别
讲了这么多,最后给大家总结一句好记的话:
原始类型用ref,一组相关引用类型用reactive;要换整个数据用ref,只改属性用reactive;JavaScript里ref要加.value,reactive不用;template里都不用加。
也不用太纠结,其实很多时候两个都能用,比如存表单数据,用ref包装一个对象也可以,用reactive也可以,根据自己的习惯和场景选就行,不过按照上面的适用场景选,代码会更简洁,更不容易踩坑。
好了,今天的内容就到这里,如果你还有其他关于Vue3的问题,欢迎在评论区留言讨论!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


