不少刚学Vue3的同学,总会纠结 ref 和 reactive 该咋选。明明都是做响应式,为啥场景不一样?它们在原理、使用细节上到底差在哪?今天就用问答的方式,把这俩API的区别掰碎了讲清楚~
ref和reactive的“本职工作”是啥?
先理解核心定位:都是Vue3实现响应式数据的API,但适用的数据类型、包装逻辑不一样。
ref
它像个“万能容器”,能给「基本类型(字符串、数字、布尔等)」或「对象/数组(引用类型)」做响应式包装,核心靠 .value
触发更新——修改时要通过 .value
访问,模板里却不用写(Vue会自动解包)。
举个例子:想让数字 count
变化时界面更新,得这么写:
const count = ref(0) // 包装基本类型number count.value++ // 修改时必须通过 .value
要是用ref包对象,内部会自动用reactive再包一层(原理部分会讲),所以修改对象属性也能响应式:
const user = ref({ name: '小明' }) user.value.name = '小红' // 触发界面更新
reactive
它专门给「对象/数组(引用类型)」做响应式,对基本类型(单独的字符串、数字)无效,原理是基于ES6的 Proxy
,把整个对象“裹一层代理”,访问、修改属性时,Proxy会拦截并触发更新。
比如给对象做响应式:
const user = reactive({ age: 18 }) user.age = 19 // 直接修改属性,自动触发更新
但如果硬塞基本类型给reactive(const num = reactive(10)
),Vue会默默忽略,num的变化不会触发界面更新——因为Proxy没法代理单个值~
啥场景下选ref,啥场景选reactive?
实际开发中,选哪个API得看数据类型、操作逻辑甚至团队风格,分几个典型场景说:
场景1:处理基本类型 → 必须用ref
reactive对 string
/number
/boolean
这些“单个值”无效(Proxy只能代理对象),所以只要是基本类型(比如计数器数字、开关布尔值、用户名字符串),只能用ref包。
场景2:处理简单对象/数组 → reactive更“原生”
如果对象结构简单(比如只有一层属性的 {name: '', age: 0}
),用reactive写起来像操作普通对象,不用频繁写 .value
,比如表单数据绑定:
const form = reactive({ username: '', password: '' }) // 模板里直接用 form.username,逻辑里也直接 form.username = 'xxx'
但对象结构复杂(嵌套三四层)时,用ref包reactive对象更灵活(后面嵌套部分会讲)。
场景3:组合式函数(Composables)返回数据 → 优先用ref
组合式函数是Vue3复用逻辑的核心,比如写个 useCounter
函数,要把内部响应式数据暴露给外部组件,这时用ref更友好:ref的 .value
在模板里会自动解包,外部用的时候不用关心 .value
,风格更统一。
举个简化的组合式函数:
export function useCounter() { const count = ref(0) const increment = () => count.value++ return { count, increment } } // 组件里用的时候: const { count, increment } = useCounter() // 模板里直接写 {{ count }},不用 {{ count.value }}
场景4:需要“替换整个对象” → 必须用ref
reactive代理的对象有个大坑:不能直接整个替换。
const user = reactive({ age: 18 }) user = { age: 20 } // 危险!原来的Proxy代理没了,新对象不是响应式的
但ref可以安全替换整个对象(因为ref的 .value
是“容器”,替换容器内容不影响响应式):
const user = ref({ age: 18 }) user.value = { age: 20 } // 新对象会被自动reactive化,后续修改仍能触发更新
响应式原理:ref和reactive咋实现“数据变化触发更新”?
想彻底分清两者,得懂底层逻辑(不用死记,理解后选API更顺)。
ref的原理:“包装对象+value劫持”
Vue给ref做了个“包装对象”,只有一个 value
属性,给基本类型用ref时,Vue通过类似 Object.defineProperty
的方式,对 value
的读写做拦截——读取时收集依赖,修改时触发更新。
哪怕用ref包对象,内部逻辑是:把对象传给reactive做代理,再把代理后的对象塞到ref的value里,所以修改 refObj.value.xxx
时,本质是修改reactive代理对象的属性,自然能触发更新。
reactive的原理:“Proxy全对象代理”
reactive基于ES6的 Proxy
,直接对整个对象做代理,不管是读属性、改属性、删属性,Proxy的拦截器(get/set/deleteProperty等)都会捕获操作,进而触发响应式更新,但Proxy有个前提:只能代理对象(对象、数组、Map等引用类型),所以基本类型塞进去没用——单个值没法被Proxy拦截。
嵌套数据更新时,ref和reactive的“写法差异”有多大?
实际开发中,对象嵌套(比如用户信息里套地址,地址里套城市)是常态,这时候两者的使用细节特别容易踩坑。
用ref处理嵌套对象:必须“逐层.value”
假设用ref包了个多层嵌套对象:
const user = ref({ info: { name: '小明', address: { city: '北京' } } }) // 想修改城市为上海,必须这样写: user.value.info.address.city = '上海' // 因为 user 是ref包装的,要先通过 .value 拿到内部的reactive对象,再逐层改属性
用reactive处理嵌套对象:直接改深层属性
reactive代理的对象,所有属性操作都会被Proxy拦截,所以修改深层属性不用额外处理:
const user = reactive({ info: { name: '小明', address: { city: '北京' } } }) // 直接修改深层属性,Proxy能捕获到: user.info.address.city = '上海' // 自动触发界面更新,不用写.value
延伸坑点:ref替换整个对象会丢响应式吗?
不会!
const user = ref({ age: 18 }) user.value = { age: 20 } // 新对象会被自动reactive化 user.value.age = 21 // 依然能触发更新
但reactive如果直接替换整个对象,就会丢响应式:
const user = reactive({ age: 18 }) user = { age: 20 } // 原来的Proxy代理没了,新对象不是响应式的 user.age = 21 // 界面不会更新!
TypeScript 里,ref和reactive的类型推导有啥不同?
很多同学用Vue3+TS开发时,类型提示是刚需,这时候ref和reactive的区别更明显:
ref的类型推导:泛型支持“丝滑无比”
ref天然支持泛型,不管是显式指定类型,还是让TS自动推导,都很直观:
// 显式指定类型为number const count = ref<number>(0) // TS自动推导:count的类型是 Ref<number> const count = ref(0) // 配合接口约束对象 interface User { name: string; age: number } const user = ref<User>({ name: '小明', age: 18 })
reactive的类型推导:需要“主动贴类型”
reactive是Proxy代理对象,TS对“代理后的对象”类型推导没那么智能,尤其是空对象容易变成any
,所以得主动约束类型:
// 危险写法:空对象会被推导成 any const user = reactive({}) // 正确写法1:用接口约束 interface User { name: string; age: number } const user = reactive<User>({ name: '小明', age: 18 }) // 正确写法2:先定义对象再代理(利用TS的类型推导) const rawUser = { name: '小明', age: 18 } as const const user = reactive(rawUser) // 此时user的类型是 { name: '小明'; age: 18 },不用额外写接口
实际开发中,还有哪些容易混淆的细节?
除了核心区别,这些细节能帮你快速避坑:
模板自动解包:ref不用写.value,reactive直接用属性
ref在模板里会被自动解包,
<!-- 组件里用ref的count --> {{ count }} <!-- 等价于 {{ count.value }} -->
但reactive代理的对象,模板里直接用属性名:
<!-- 组件里用reactive的user --> {{ user.age }} <!-- 直接访问,因为user是被Proxy代理的对象 -->
数组处理:ref和reactive都能响应式,但修改方式不同
用ref包数组:
const list = ref([1, 2, 3]) list.value[0] = 0 // 必须通过 .value 访问数组,修改某一项
用reactive包数组:
const list = reactive([1, 2, 3]) list[0] = 0 // 直接修改,Proxy能拦截到
性能差异:日常开发不用纠结
有人担心reactive代理大对象有性能问题,或ref多一层value包装影响性能,但Vue3对响应式的优化已很到位,Proxy的拦截开销和ref的value包装开销,在实际项目中完全感知不到——不用为这点差异强行选API,按场景选更重要。
最后总结:记住这张“选择地图”
为了方便记忆,把场景和API的对应关系浓缩成一张表,以后遇到需求直接查:
场景 | 选ref还是reactive? | 理由 |
---|---|---|
基本类型(string/number等) | 必须ref | reactive无法代理基本类型,ref的.value能包装单个值并触发响应式 |
简单对象/数组(结构稳定) | reactive更顺手(少写.value) | 直接代理对象,读写属性像操作普通对象,代码更简洁 |
复杂嵌套对象(可能动态替换整个对象) | ref + reactive 结合(或只用ref) | ref的.value能安全替换整个对象,避免reactive赋值丢失响应式的问题;如果对象嵌套深,ref包reactive更灵活 |
组合式函数暴露数据 | 优先ref | 外部使用时,模板自动解包.value,API风格更统一 |
TypeScript类型约束严格的场景 | ref更省心(泛型支持友好) | ref的泛型写法直观,reactive需要手动约束对象类型,否则容易出现any |
理解透这些区别后,写Vue3代码时就不用再纠结“该用ref还是reactive”——看场景选API,代码逻辑和响应式更新都能稳稳拿捏~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。