vue ref和reactive的区别
下面我把 ref 和 reactive 在 Vue 3(Composition API)中的差异、原理、常见用法与坑都讲清楚——带示例、对比和实战建议,方便你直接拿去用。 核心结论(先看要点)
- ref:用来包裹 任意值(常用于原始值 Number/String/Boolean),返回 { value } 的响应式容器。对对象也可以用 ref(obj),但它是“以 ref 的形式”持有该对象。模板里会自动解 .value。
- reactive:接收一个 对象/数组/Map/Set,返回该对象的 Proxy(深度响应式),直接使用属性访问,无需 .value。
- 选用建议:简单原始值用 ref;结构化对象/集合用 reactive。需要在 Composition API 中解构响应式对象时,使用 toRefs / toRef 保持响应性。
原理简述(简单版)
- Vue 的响应式基于 Proxy。reactive(obj) 返回对 obj 的代理(proxy),拦截读取/写入来收集依赖并触发更新(深层追踪)。
- ref(x) 返回一个对象 { value: reactiveWrappedValue }。当 value 被读取/写入时,会进行依赖追踪与触发。对于对象 ref(obj),内部会对对象进行响应式处理(默认深度),但它的外层仍是一个 ref 容器。
代码对比(最小示例)
▼js复制代码import { ref, reactive } from 'vue' // ref 用法(原始值) const count = ref(0) count.value++ // 在 JS 里要用 .value // 在模板中: {{ count }} // 模板会自动解包,不需要 .value // reactive 用法(对象) const state = reactive({ a: 1, b: { c: 2 } }) state.a = 2 // 直接更新属性,响应式生效 // 深层属性 state.b.c 也是响应式的
深入对比:行为与细节
- 支持的类型
- reactive:设计用于对象 ({}/[]/Map/Set)。对非对象使用并不合适(通常直接使用 ref)。
- ref:任意类型(原始值、对象、数组 等)。
- 访问方式
- ref:在 JS 中必须 .value(模板自动解包)。
- reactive:直接 state.prop。
- 深度(是否递归)
- reactive:深度响应式(内部所有嵌套对象都会被代理)。
- ref(obj):外层是 ref,但 .value 的对象也会被转换为响应式(深度),除非使用 shallowRef。
- 解构丢失响应性(常见坑) const state = reactive({ a: 1, b: 2 }) const { a } = state // 解构得到的是普通变量,不会响应 // 正确做法: import { toRefs } from 'vue' const { a } = toRefs(state) // a 是 ref,保持响应性 对 ref 解构通常不是问题,因为你通常不会解构 .value。
- 判断与转换工具
- isRef(x)、isReactive(x):判断。
- unref(x):如果是 ref 则返回 .value,否则返回 x。
- toRef(obj, 'key') / toRefs(obj):把对象属性转成 ref 便于解构。
与模板的交互
- 模板会自动 unwrap ref(所以模板中写 {{ count }} 即可)。
- reactive 在模板中直接使用属性:{{ state.a }}。
与 watch / computed 的配合
- watch 可以监听 ref 或 reactive(监听整个对象需要传 getter 或 deep: true)。
- computed 接受函数或 getter,返回 Readonly ref。 示例: watch(() => state.a, (newVal) => { ... }) // 监听 reactive 的某个字段 watch(count, (v) => { ... }) // 监听 ref
特殊 API:shallowRef / shallowReactive / customRef
- shallowRef:只对 .value 做响应,不会对内部对象做深度转换(适合大对象或第三方不可代理对象)。
- shallowReactive:只对第一层做响应,内部嵌套对象不再递归代理。
- customRef:自定义响应行为(节流、防抖等场景)。
与 Object.assign / 解构 / 扩展运算符 的关系(你之前的问题) Object.assign({}, obj1, obj2) 或 { ...obj1 } 会创建一个 新普通对象,拷贝的是 当前值,不会保留原来的 Proxy/响应式连接。举例:
▼js复制代码const a = reactive({ x: 1 }) const merged = Object.assign({}, a) // merged 不是响应式 const spreaded = { ...a } // 也不是响应式
// 如果想得到响应式对象:
▼js复制代码const mergedReactive = reactive(Object.assign({}, a))
如果你想把 reactive 对象的属性合并到另一个 reactive 上,直接修改属性会保留响应性:
▼js复制代码Object.assign(state, other) // 如果 state 和 other 已是 reactive,直接修改 state 的属性会触发响应
常见误区 & 实战建议
- 原始值优先用 ref(比如 count, isOpen)。
- 复杂状态对象/集合用 reactive(例如 user = {profile, settings}、数组数据 list)。
- 解构 reactive 时丢失响应性:用 toRefs() 或在模板/函数中直接访问 state.prop。
- 想替换整个对象:如果你需要替换整个 state(例如 state = newObj),reactive 不能被重新赋值(通常 const state = reactive(...)),你应把需要替换的字段逐一更新,或用 ref 持有对象然后 objRef.value = newObj(这样替换会生效)。
- 性能考虑:对大对象或第三方对象使用 shallowRef/shallowReactive,避免不必要的深度代理开销。
- 模板里不用 .value,但在 JS 里要注意区分(或使用 unref / toRefs 帮助转换)。
常见场景举例 场景 A:计数器
▼js复制代码const count = ref(0) count.value++
场景 B:表单对象
▼js复制代码const form = reactive({ name: '', age: 0 }) // 方便:直接 form.name = 'x'
场景 C:需要替换整个对象(例如从接口拿到新对象)
▼js复制代码const userRef = ref({}) // 用 ref 包裹对象,方便整体替换 userRef.value = await fetchUser()
或者: // 如果用 reactive:
▼js复制代码Object.assign(user, await fetchUser()) // 更新属性而不是替换引用
场景 D:组合使用(推荐)
▼js复制代码const state = reactive({ items: [] }) const loading = ref(false) // 清晰:state 用于结构化数据,loading 用于原始布尔
快速 API 备忘(常用函数)
- ref(value), reactive(obj), readonly(x)
- isRef(x), isReactive(x), unref(x)
- toRef(obj, key), toRefs(obj)
- shallowRef, shallowReactive, customRef
- markRaw(obj):标记对象不被代理
- proxyRefs(obj):把对象属性的 ref 自动解包(常用于返回 setup 的对象,便于模板使用)
总结(一句话)
- ref 是值的容器(灵活,常用于原始值及需要整体替换的对象);reactive 是对象的 Proxy(更自然地操作对象属性)。了解两者的访问方式(.value vs 直接属性)、解构坑(toRefs)和浅/深区别,就能在实际项目中正确选择。
toRef 和 toRefs的区别 toRef 和 toRefs 是 Vue 3 响应式系统中非常关键的两个工具函数,主要用于 保持响应性 —— 特别是在你从 reactive 对象中“解构属性”时。 下面我会详细讲它们的 区别、用途、源码原理,再配上清晰的图和示例。
🧩 一句话区别总结 暂时无法在飞书文档外展示此内容
🧠 为什么要有它们? 当你从一个 reactive 对象中“解构属性”时,响应性会丢失。 ❌ 普通解构会丢失响应性
▼js复制代码const state = reactive({ count: 0, name: '小明' }) const { count } = state // ❌ count 不是响应式的 count++ // 修改不会触发视图更新
原因: 解构后得到的是普通值,而不是响应式引用(Vue 的 Proxy 失效了)。
✅ 解决办法:toRef
▼js复制代码import { reactive, toRef } from 'vue' const state = reactive({ count: 0 }) const count = toRef(state, 'count') console.log(count.value) // 0 count.value++ console.log(state.count) // 1 ✅ 保持同步
✨ 特点:
- toRef(state, 'count') 返回一个 ref 对象,其 .value 始终指向 state.count。
- 它 不会创建新的值或副本,只是创建了一个“引用镜像”。
- 常用于“传递单个属性”给子组件或 watch。
✅ 多属性情况:toRefs
▼js复制代码import { reactive, toRefs } from 'vue' const state = reactive({count: 0,name: '小明' }) // 把所有属性都转成 ref const { count, name } = toRefs(state) count.value++ name.value = '小东' console.log(state.count) // 1 ✅ console.log(state.name) // 小东 ✅ ✨ 特点: - 它会遍历对象的所有 key,执行 toRef(state, key)。 - 返回的对象与原对象 结构相同,但每个属性都是 ref。 - 常用于在 setup() 里返回响应式状态给模板: return { ...toRefs(state) // 模板中直接用 {{ count }} {{ name }} }
⚙️ 底层关系图(简化版) ┌────────────────────────┐ │ reactive对象 state │ │ { │ │ count: 0, │ │ name: "小明" │ │ } │ └────────────┬───────────┘ │ ├── toRef(state, "count") → Ref对象 { value ↔ state.count } │ └── toRefs(state) → { count: Ref, name: Ref }
🧩 常见使用场景对比 ✅ 场景 1:只需要一个属性(推荐用 toRef)
▼js复制代码const user = reactive({ name: 'Tom', age: 18 }) const age = toRef(user, 'age') // watch 单个字段 watch(age, (v) => console.log('年龄变化', v))
✅ 场景 2:需要在模板中用多个属性(推荐用 toRefs)
▼js复制代码const user = reactive({ name: 'Tom', age: 18 }) return { ...toRefs(user) // 模板中直接用 {{ name }} {{ age }} } ---
✅ 场景 3:嵌套响应式对象
▼js复制代码const state = reactive({user: { name: 'Tom', age: 18 } }) const user = toRef(state, 'user') // user 是一个 ref,指向整个对象 user.value.age++ // 更新响应式
