Code前端首页关于Code前端联系我们

不少刚学Vue3的同学,总会纠结 ref 和 reactive 该咋选。明明都是做响应式,为啥场景不一样?它们在原理、使用细节上到底差在哪?今天就用问答的方式,把这俩API的区别掰碎了讲清楚~

terry 2周前 (10-02) 阅读数 56 #Vue
文章标签 Vue3;ref reactive

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前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门