Vue 3 响应式基础:refreactivetoReftoRefs

系列导航:Vue 3 现代开发指南
上一篇:Composition API 与 <script setup>
下一篇:派生状态与副作用

Vue 3 使用 Proxy 实现对象响应式,并通过 ref 为任意值提供统一的响应式容器。实际项目中,最稳妥的默认策略是:优先用 ref 表达可能被整体替换的状态,使用 reactive 表达边界清晰、主要按属性修改的对象。

一、ref:统一的响应式容器

ref() 可以接收基本类型或对象:

import { ref } from 'vue'

const count = ref(0)
const title = ref('Vue 3')
const user = ref({ id: 1, name: 'Ada' })

脚本中通过 .value 读写:

count.value++
title.value = 'Vue 3.5'
user.value.name = 'Grace'
user.value = { id: 2, name: 'Lin' }

模板会自动解包顶层 ref

<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button type="button" @click="count++">{{ count }}</button>
</template>

变量本身通常声明为 constconst 限制的是 count 不能指向另一个 ref,不妨碍修改 count.value

二、reactive:对象的深层响应式代理

reactive() 只接受对象类型,返回原对象的 Proxy:

import { reactive } from 'vue'

const form = reactive({
  profile: { name: '', city: '' },
  tags: [] as string[],
})

form.profile.name = 'Ada'
form.tags.push('vue')

默认情况下,嵌套对象也会在访问时转为响应式代理。基本类型应使用 ref,不要写 reactive(0)

三、为什么不能直接替换 reactive 变量

响应式连接存在于 Proxy 上。把变量改指向普通对象,会失去原代理:

let form = reactive({ name: '', city: '' })

// 不推荐:新对象没有沿用模板正在追踪的原代理
form = { name: 'Ada', city: 'London' }

如果对象经常整体替换,用 ref

const form = ref({ name: '', city: '' })

form.value = { name: 'Ada', city: 'London' }

如果希望保留同一个代理,更新已有属性:

const form = reactive({ name: '', city: '' })

Object.assign(form, { name: 'Ada', city: 'London' })

Object.assign() 不会自动删除源对象里不存在的旧字段,因此它不等价于严格的对象替换。

四、如何选择 refreactive

场景 推荐
字符串、数字、布尔值 ref
数据可能整体替换 ref
异步请求结果 ref
表单对象主要按字段修改 reactiveref 均可
希望保持统一心智模型 优先 ref

“对象层级深就一定用 reactive”不是必要规则。对象放进普通 ref 后,内部同样会转为深层响应式。

五、解构为什么会丢失响应式

直接解构 reactive 对象的基本类型属性,只会得到当时的普通值:

const state = reactive({ count: 0, name: 'Ada' })
const { count } = state

state.count++
console.log(count) // 仍然是 0

需要独立引用属性时,使用 toRef()

import { reactive, toRef } from 'vue'

const state = reactive({ count: 0, name: 'Ada' })
const count = toRef(state, 'count')

count.value++
console.log(state.count) // 1

批量转换现有属性时,使用 toRefs()

import { reactive, toRefs } from 'vue'

const state = reactive({ count: 0, name: 'Ada' })
const { count, name } = toRefs(state)

count.value++
name.value = 'Grace'

toRefs() 只处理调用时对象上可枚举的属性。对于当时不存在、之后才加入的属性,应直接使用 toRef(object, key)

六、Props 解构是一个特殊情况

Vue 3.5 中,编译器会让 defineProps() 的解构变量保持响应式:

<script setup lang="ts">
interface Props {
  page?: number
  pageSize?: number
}

const { page = 1, pageSize = 20 } = defineProps<Props>()
</script>

这段代码不同于普通 reactive 对象解构,是 <script setup> 编译器提供的能力。不要据此推断所有响应式对象都可以直接解构。

如果要把解构后的 prop 传给 watch,传 getter:

watch(() => page, (newPage) => {
  console.log(newPage)
})

直接 watch(page, ...) 传入的是当前数值,不是可监听的数据源。

七、ref 的自动解包边界

模板中的顶层 ref 会自动解包,但数组、Map 等原生集合里的 ref 不会自动解包:

const books = reactive([ref('Vue Guide')])
<template>
  <p>{{ books[0].value }}</p>
</template>

作为深层响应式普通对象属性的 ref 会被解包并与原 ref 保持连接:

const count = ref(1)
const state = reactive({ count })

console.log(state.count) // 1
state.count = 2
console.log(count.value) // 2

不要为了省略 .value 而刻意把 ref 塞进复杂对象。

八、TypeScript 类型写法

通常让 TypeScript 从初始值推断类型即可:

const count = ref(0) // Ref<number>
const user = reactive({ id: 1, name: 'Ada' })

值可能为空时显式声明联合类型:

interface User {
  id: number
  name: string
}

const selectedUser = ref<User | null>(null)

不要用不真实的类型断言掩盖运行时空值。

九、一个实际表单示例

<script setup lang="ts">
import { computed, reactive, ref } from 'vue'

interface ProfileForm {
  name: string
  email: string
}

const form = reactive<ProfileForm>({ name: '', email: '' })
const submitting = ref(false)

const isValid = computed(
  () => form.name.trim().length > 0 && form.email.includes('@'),
)

async function submit() {
  if (!isValid.value || submitting.value) return

  submitting.value = true
  try {
    await saveProfile({ ...form })
  } finally {
    submitting.value = false
  }
}

async function saveProfile(payload: ProfileForm) {
  console.log(payload)
}
</script>

<template>
  <form @submit.prevent="submit">
    <input v-model.trim="form.name" placeholder="姓名" />
    <input v-model.trim="form.email" type="email" placeholder="邮箱" />
    <button :disabled="!isValid || submitting">
      {{ submitting ? '提交中…' : '保存' }}
    </button>
  </form>
</template>

表单字段适合放在 reactive 对象里,提交状态则是独立的 ref。两种 API 可以自然组合,不需要选边站。

小结

ref 能表达所有类型并支持整体替换,是很好的默认选择;reactive 适合围绕同一对象逐字段修改。普通响应式对象直接解构会断开基本类型属性的响应式连接,需要 toRef()toRefs();Vue 3.5 的 Props 响应式解构属于编译器特例。

官方资料

系列导航:目录 · 上一篇 · 下一篇