Skip to content

D05 · ref vs reactive:如何选择 ​

对应主课: L03 响应式基础、L32 依赖追踪原理 最后核对: 2026-09-23


1. 机制区别 ​

维度ref()reactive()
接受值类型可保存原始值或对象支持的对象类型,如普通对象、数组、Map/Set;不含原始值
访问方式.value直接访问属性
拦截机制getter/setterProxy
解构保留 ref 对象可保留连接;复制 .value 中的普通值也会断开普通属性解构得到快照;嵌套对象仍可能是响应式代理
替换整体.value 赋值会通知其消费者重赋普通变量本身不触发通知;可更新代理属性或使用 ref 容器
模板中顶层模板绑定会解包,嵌套表达式有边界通过代理属性访问

ref 自身通过访问器追踪 .value;普通 ref 包含对象时,内部对象再转为响应式。Map/Set 有专门的 handler,不是任意内建对象都可以代理后照常使用。ref/reactive API


2. 解构问题 ​

typescript
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.value

toRefs 为调用时可枚举的已有属性创建链接;可选字段还不存在时,可以使用 toRef(state, 'field')。传入 props 等只读来源后,得到 ref 也不意味着可以修改父级状态。toRefs


3. 替换整体 ​

typescript
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”,而应说明重赋变量缺少触发通知的入口。稳定代理也可以更新属性:

typescript
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 当数组:

typescript
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. 选择指南 ​

简单原则 ​

typescript
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 则保留内部原值:

typescript
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:

typescript
// 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 建议