Vue 中 ref 和 reactive 有什么区别?该用哪个?
Vue 中 ref 和 reactive 有什么区别?该用哪个?
一句话总结:ref 适合「基本类型 + 需要重新赋值」的场景,reactive 适合「对象/数组 + 不需要整体替换」的场景。记不住?基本类型用 ref,对象用 reactive,需要整体替换的用 ref 包对象,就这么简单!
正文目录
- ref 和 reactive 的核心区别是什么?
- 5 大高频场景 & 代码示例
- 万能兜底:ref vs reactive 决策表
- 预防 checklist(不再踩坑)
- 一句话总结
一、ref 和 reactive 的核心区别是什么?
问题背景:Vue 3 的响应式系统基于Proxy,提供了ref和reactive两种创建响应式数据的 API,很多开发者分不清什么时候用哪个。
| 对比项 | ref | reactive |
|---|---|---|
| 支持类型 | 任意类型(基本类型 + 对象) | 仅对象/数组 |
| 访问方式 | .value(模板中自动解包) | 直接访问属性 |
| 重新赋值 | ✅ 直接xxx.value = newVal | ❌ 丢失响应式 |
| 解构 | 需要配合toRefs | 直接解构会丢失响应式 |
| 模板中使用 | 自动解包,无需.value | 直接使用 |
| 深层响应 | ✅ 默认深层 | ✅ 默认深层 |
import{ref,reactive}from'vue'// ✅ ref:基本类型constcount=ref(0)count.value++// JS 中需要 .value// 模板中:{{ count }} 自动解包,不需要 .value// ✅ reactive:对象conststate=reactive({name:'Claw',age:3})state.age=4// 直接修改属性// ❌ reactive 整体替换 → 丢失响应式state=reactive({name:'New',age:5})// 不报错但响应式断了二、5 大高频场景 & 代码示例
场景 1:计数器 / 开关 → 用 ref
基本类型,需要频繁修改,用 ref 最直观。
// ✅ 正确:基本类型用 refconstisLoading=ref(false)constcount=ref(0)constmessage=ref('hello')functiontoggle(){isLoading.value=!isLoading.value}// ❌ 错误:reactive 不支持基本类型constcount=reactive(0)// Vue 警告:value should be an object场景 2:表单对象 → 用 reactive
多个关联字段组成一个对象,不需要整体替换,用 reactive 更自然。
<script setup> import { reactive } from 'vue' // ✅ 表单对象用 reactive const form = reactive({ username: '', password: '', remember: false }) function submit() { console.log(form.username, form.password) } function reset() { // ⚠️ 逐字段重置,不能整体替换 form.username = '' form.password = '' form.remember = false } </script>// ❌ 错误:整体替换丢失响应式functionreset(){form={username:'',password:'',remember:false}// 响应式断了!}场景 3:需要整体替换的对象 → 用 ref
从接口拿数据后整体赋值,用 ref 包对象更安全。
// ✅ 正确:接口数据用 ref,方便整体替换constuserInfo=ref(null)asyncfunctionfetchUser(){constres=awaitapi.getUser()userInfo.value=res.data// ✅ 整体替换没问题}// ❌ 麻烦:reactive 要逐字段赋值constuserInfo=reactive({})asyncfunctionfetchUser(){constres=awaitapi.getUser()Object.assign(userInfo,res.data)// 能用但啰嗦// 或者逐字段:userInfo.name = res.data.name ...}场景 4:解构响应式对象 → 用 toRefs 配合 reactive
reactive 解构后会丢失响应式,必须用toRefs包装。
<script setup> import { reactive, toRefs } from 'vue' const state = reactive({ name: 'Claw', age: 3 }) // ❌ 直接解构:丢失响应式 // const { name, age } = state // ✅ toRefs 解构:保持响应式 const { name, age } = toRefs(state) </script>// ref 则不需要 toRefsconstcount=ref(0)// 模板中直接用 {{ count }},无需解构场景 5:watch 监听 → ref 和 reactive 行为不同
import{ref,reactive,watch}from'vue'// ✅ ref:watch 默认监听 .value 的变化constcount=ref(0)watch(count,(newVal,oldVal)=>{console.log(`${oldVal}→${newVal}`)})// ✅ reactive:watch 默认深层监听conststate=reactive({count:0})watch(state,(newVal)=>{console.log('state changed',newVal.count)})// ⚠️ watch reactive 的某个属性 → 需要用 getterwatch(()=>state.count,(newVal,oldVal)=>{console.log(`${oldVal}→${newVal}`)})// ⚠️ ref 包对象 → 默认不深层监听,需要 deepconstobj=ref({count:0})watch(obj,(newVal)=>{console.log(newVal)// obj.value = {} 时触发},{deep:true})// 修改 obj.value.count 需要 deep三、万能兜底:ref vs reactive 决策表
| 场景 | 用什么 | 原因 |
|---|---|---|
| 基本类型(string/number/boolean) | ref | reactive 不支持基本类型 |
| 独立的对象/数组,不需要整体替换 | reactive | 访问自然,不需要.value |
| 接口返回的数据,需要整体替换 | ref | 可以直接xxx.value = newData |
| 表单对象 | reactive | 字段关联,逐字段操作 |
| 需要解构使用的对象 | reactive+toRefs | ref 不需要解构 |
| 定时器/ DOM 引用 | ref | 存任意值,不触发渲染 |
| 组合式函数返回值 | ref | 调用方可以解构后保持响应式 |
万能决策流程:
是基本类型吗? → 是 → 用 ref → 否 → 需要整体替换吗? → 是 → 用 ref(包对象) → 否 → 用 reactive// 万能模板import{ref,reactive}from'vue'// 基本类型constloading=ref(false)consttotal=ref(0)// 对象 - 不需要整体替换constform=reactive({name:'',email:''})// 对象 - 需要整体替换(接口数据)constlist=ref([])asyncfunctionloadData(){list.value=awaitapi.getList()}四、预防 checklist(不再踩坑)
- 基本类型必须用
ref,不要用reactive reactive对象不要整体替换,用Object.assign或逐字段赋值- 接口数据整体赋值 → 用
ref包对象 - 解构
reactive→ 必须用toRefs或toRef - 模板中
ref自动解包,JS 中必须.value watch监听reactive属性 → 用 getter 函数() => state.xxxwatch监听ref对象的深层变化 → 加{ deep: true }- 组合式函数返回值优先用
ref,方便调用方解构
五、一句话总结
基本类型用 ref,固定结构对象用 reactive,需要整体替换的对象用 ref 包裹——记不住就看要不要重新赋值,要重新赋值就用 ref。
最后问候亲爱的朋友们,并邀请你们阅读我的全新著作
📚 《Vue.js 3企业级项目开发实战(微课视频版》
