D05 · ref vs reactive:如何选择
对应主课: L03 响应式基础、L32 依赖追踪原理 最后核对: 2026-09-23
1. 机制区别
| 维度 | ref() | reactive() |
|---|---|---|
| 接受值类型 | 可保存原始值或对象 | 支持的对象类型,如普通对象、数组、Map/Set;不含原始值 |
| 访问方式 | .value | 直接访问属性 |
| 拦截机制 | getter/setter | Proxy |
| 解构 | 保留 ref 对象可保留连接;复制 .value 中的普通值也会断开 | 普通属性解构得到快照;嵌套对象仍可能是响应式代理 |
| 替换整体 | .value 赋值会通知其消费者 | 重赋普通变量本身不触发通知;可更新代理属性或使用 ref 容器 |
| 模板中 | 顶层模板绑定会解包,嵌套表达式有边界 | 通过代理属性访问 |
ref 自身通过访问器追踪 .value;普通 ref 包含对象时,内部对象再转为响应式。Map/Set 有专门的 handler,不是任意内建对象都可以代理后照常使用。ref/reactive API
2. 解构问题
import { reactive, ref, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Vue', nested: { score: 1 } })
let { count: localCount } = state
localCount++ // 只改局部数字;state.count 仍为 0
const { count: countRef, name: nameRef } = toRefs(state)
countRef.value++ // state.count 变为 1
nameRef.value = 'Vue 3'
const { nested } = state
nested.score++ // nested 仍是同一个响应式对象
// 若以后替换 state.nested,nested 这个局部引用仍指向旧对象。
const independent = ref(0)
const alias = independent
alias.value++ // 保留同一个 ref 容器
let { value: snapshot } = independent
snapshot++ // 复制普通数字,不改变 independent.valuetoRefs 为调用时可枚举的已有属性创建链接;可选字段还不存在时,可以使用 toRef(state, 'field')。传入 props 等只读来源后,得到 ref 也不意味着可以修改父级状态。toRefs
3. 替换整体
import { reactive, ref } from 'vue'
const user = ref({ name: 'Vue', age: 3 })
user.value = { name: 'Vite', age: 1 } // 通知读取过 user.value 的消费者
let state = reactive({ name: 'Vue', age: 3 })
state = reactive({ name: 'Vite', age: 1 }) // 普通变量赋值,没有响应式 setter第二段不会主动让订阅旧代理的 effect 重新执行。若某个闭包因其他依赖再次运行,它可能重新读到新变量;因此不应笼统说“模板永远绑定旧 state”,而应说明重赋变量缺少触发通知的入口。稳定代理也可以更新属性:
const form = reactive({ name: 'Vue', age: 3 })
Object.assign(form, { name: 'Vite', age: 1 })
// 代理身份不变;Object.assign 不会删除新对象中缺少的旧字段。
const holder = reactive({ user: { name: 'Vue', age: 3 } })
holder.user = { name: 'Vite', age: 1 } // 改的是代理属性,可以被追踪API 列表经常需要整组替换,用 ref 表达很直接。沿用 L21 的真实接口时,取业务响应的 data 字段,不能把整个 envelope 当数组:
import { ref } from 'vue'
import { productApi } from '@/api/products'
import type { Product } from '@/types/product'
const products = ref<Product[]>([])
async function loadProducts() {
const response = await productApi.getList()
products.value = response.data
}此片段只展示赋值;页面的加载/错误/取消和竞态处理仍使用 L21 的 useRequest。若需要保留数组代理身份,splice 更新也合法,选择取决于已有消费者需要哪种引用,而不是语法长短。
4. 选择指南
简单原则
import { ref, reactive } from 'vue'
import type { User } from '@/types/auth'
import type { Product } from '@/types/product'
// 原则 1: 原始值 → ref
const count = ref(0)
const name = ref('')
const isOpen = ref(false)
// 原则 2: 需要替换整体的对象 → ref
const user = ref<User | null>(null)
const apiData = ref<Product[]>([])
// 原则 3: 紧密关联的表单字段 → reactive
const form = reactive({
username: '',
password: '',
confirmPassword: '',
})
// 原则 4: 可先用 ref 统一容器方式,再按状态归属决定是否分组5. 内部关系
普通 ref 会把可响应式化的对象转为代理,shallowRef 则保留内部原值:
import { ref, reactive, shallowRef, isReactive } from 'vue'
const obj = ref({ name: 'Vue' })
console.log(isReactive(obj.value)) // true
obj.value.name = 'Vite'
const state = reactive({ name: 'Vue' })
state.name = 'Vite'
const shallow = shallowRef({ name: 'Vue' })
shallow.value.name = 'Vite' // 内部普通属性变化不触发 shallowRef 的消费者
shallow.value = { name: 'New' } // 换 .value 才触发模板与对象解包也有边界:顶层模板绑定 count 可写 ;但不能由此推断任意嵌套表达式都自动解包。深 reactive 普通对象的 ref 属性会解包,响应式数组和 Map 中的 ref 元素仍需要 .value。解包细节
可运行的连接实验
保存到 L31 的独立 Vue 3.5.43 实验目录,执行 npx tsx ref-reactive-check.ts:
// labs/mini-reactivity/ref-reactive-check.ts
import assert from 'node:assert/strict'
import { ref, reactive, toRefs, effect, stop, shallowRef } from 'vue'
const original = reactive({ count: 0, nested: { score: 1 } })
let current = original
let seen = -1
let runs = 0
const runner = effect(() => { runs++; seen = current.count })
current = reactive({ count: 10, nested: { score: 2 } })
current.count++
assert.equal(runs, 1)
assert.equal(seen, 0)
runner() // 显式重跑,重新读到 current 并收集它的依赖
current.count++
assert.equal(seen, 12)
stop(runner)
let { count: snapshot } = original
snapshot++
assert.equal(original.count, 0)
const { count: linked } = toRefs(original)
linked.value++
assert.equal(original.count, 1)
const user = ref({ name: 'A' })
let seenName = ''
const userEffect = effect(() => { seenName = user.value.name })
user.value = { name: 'B' }
assert.equal(seenName, 'B')
stop(userEffect)
const shallow = shallowRef({ count: 0 })
let shallowCount = -1
const shallowEffect = effect(() => { shallowCount = shallow.value.count })
shallow.value.count++
assert.equal(shallowCount, 0)
shallow.value = { count: 2 }
assert.equal(shallowCount, 2)
stop(shallowEffect)
const boxed = ref(3)
const object = reactive({ boxed })
const array = reactive([boxed])
assert.equal(object.boxed, 3)
assert.equal(array[0]!.value, 3)
console.log('ref 容器、代理属性与普通变量的连接检查通过')6. 总结
| 推荐 ref 的场景 | 推荐 reactive 的场景 |
|---|---|
| 原始值 | 表单对象 |
| API 返回值 | 配置对象 |
| 可能被替换的对象 | 状态紧密关联的分组 |
| 从 composable 返回 | 组件内局部状态分组 |
| 不确定时的默认选择 |
官方指南推荐 ref 作为声明响应式状态的主要 API,它可以统一处理原始值与整体替换;但从 ref.value 中复制普通值仍会得到快照。reactive 适合保留对象身份的状态分组,两者可一起使用。reactive 的限制与 ref 建议