Vue 3 响应式系统类型关系总结(附:computed、props)

Vue3提供了多种创建响应式数据的方式,主要包括

  • reactive()(返回代理对象,适用于复杂数据结构)
  • ref()(返回Ref对象,包装原始值)
  • computed()(返回计算属性Ref)

不同类型具有不同的响应式特点:reactive实现深度响应,ref需要.value访问,computed具有惰性求值特性。


此外还有readonly、shallow系列等变体,分别适用于只读、浅层响应等场景。


类型判断可使用isRef/isReactive工具函数。


开发者应根据具体需求(处理原始值/对象、性能优化、只读需求等)选择合适的响应式API。


Vue 3 响应式系统类型关系总结

类别 核心API 返回类型 响应性特点 适用场景 注意事项
响应式对象 reactive() T (代理对象) 深度响应式,嵌套属性自动响应 对象、数组、Set、Map等复杂数据结构 1. 仅适用于对象类型 2. 不能用于原始值 3. 解构会丢失响应性
响应式原始值 ref() Ref<T> 包装原始值为响应式对象,通过.value访问 原始值(string、number、boolean等)、对象引用 1. 需通过.value访问/修改值 2. 模板中自动解包
计算属性 computed() ComputedRef<T> 惰性求值,缓存结果,依赖变化时重新计算 派生状态、复杂计算逻辑 1. 避免副作用 2. 结果只读(除非提供setter)
只读响应式 readonly() Readonly<T> 创建只读代理,防止意外修改 传递响应式对象但不允许修改的场景 1. 嵌套属性也是只读的 2. 深层只读
浅层响应式 shallowRef() ShallowRef<T> .value本身响应式,嵌套对象不处理 大型对象或与第三方库集成 1. 替换整个.value会触发响应 2. 修改嵌套属性不会
浅层响应式对象 shallowReactive() T 仅根级别属性响应式,嵌套对象保持原样 性能敏感场景,明确知道哪些属性需要响应 仅跟踪根级别属性的访问/修改
浅层只读 shallowReadonly() Readonly<T> 根级别只读,嵌套对象可修改 需要部分保护的情况 允许修改深层嵌套属性
响应式转换 toRef() Ref<T> 为响应式对象的属性创建单独ref 提取props的属性保持响应性 保持与源属性的响应式连接
响应式转换(多个) toRefs() { [K in keyof T]: Ref<T[K]> } 将响应式对象转为普通对象,属性均为ref 解构reactive对象时保持响应性 便于解构而不丢失响应性
原始值转换 toRaw() T 返回reactive或readonly的原始对象 需要操作非代理对象时(如第三方库) 绕过响应式系统,修改不会触发更新
标记响应式 markRaw() T 标记对象永不转换为响应式 第三方类实例、大型不可变数据 Vue会跳过对该对象的代理

类型关系图

text

复制代码
原始数据
├── reactive() → 响应式代理 (深度)
│   ├── readonly() → 只读代理
│   └── toRefs() → 解构用ref集合
├── ref() → Ref对象 (包装值)
│   ├── shallowRef() → 浅层Ref
│   └── computed() → 计算Ref
├── shallowReactive() → 浅层代理
│   └── shallowReadonly() → 浅层只读代理
└── markRaw() → 非响应式标记

选择指南

  1. 使用 reactive 当处理对象/集合且需要深度响应

  2. 使用 ref 当处理原始值或需要替换整个对象引用

  3. 使用 computed 当值依赖其他响应式数据

  4. 使用 readonly 当需要防止意外修改

  5. 使用 shallow* 系列优化性能或集成外部库

  6. 使用 toRefs 当需要解构reactive对象


TypeScript 类型关系示例

TypeScript 复制代码
// 基本类型关系
type Reactive<T> = T extends object ? T : never
type Ref<T> = { value: T }
type ComputedRef<T> = Readonly<Ref<T>>

// 实际使用
const state = reactive({ count: 0 }) // Reactive<{ count: number }>
const count = ref(0) // Ref<number>
const doubled = computed(() => count.value * 2) // ComputedRef<number>

这个总结涵盖了Vue 3响应式系统的主要API及其类型关系,帮助开发者根据具体场景选择合适的响应式工具。


在 Vue 3 中,响应式值的类型不一定是 Ref ,但使用 ref() 创建的响应式变量确实是 Ref<T> 类型


Vue 3 提供了多种创建响应式数据的方式,每种方式对应不同的类型:


1. ref() → 类型是 Ref<T>

用于创建基本类型 (如 numberstringboolean)或普通对象的响应式引用。

TypeScript 复制代码
import { ref } from 'vue'

const count = ref(0)        // Ref<number>
const name = ref('Alice')   // Ref<string>
const obj = ref({ a: 1 })   // Ref<{ a: number }>
  • 访问/修改需要 .value

    TypeScript 复制代码
    console.log(count.value) // 0
    count.value++            // 修改

2. reactive() → 类型是 原始对象的代理(Proxy) ,不是 Ref

用于创建对象或数组的深层响应式副本。

TypeScript 复制代码
import { reactive } from 'vue'

const state = reactive({ count: 0, name: 'Alice' }) // { count: number; name: string }
  • 直接访问属性,不需要 .value

    TypeScript 复制代码
    console.log(state.count) // 0
    state.count++            // 修改

⚠️ 注意:reactive() 返回的是一个 Proxy 对象,其 TypeScript 类型就是你传入对象的类型(如 { count: number }),不是 Ref


3. shallowRef() / shallowReactive() → 浅层响应式

  • shallowRef<T>()Ref<T>(但只对 .value 本身响应,内部对象属性非响应式)
  • shallowReactive<T>(obj)T(只对顶层属性响应)

4. 在 <script setup> 中配合 defineModelprops

  • propsreactive 对象 (不可变,类型为 Readonly<...>
  • defineModel() 返回的是 Ref

如何判断一个值是不是 Ref

使用 Vue 提供的工具函数:

TypeScript 复制代码
import { isRef, isReactive } from 'vue'

isRef(count)      // true(如果是 ref 创建的)
isReactive(state) // true(如果是 reactive 创建的)

总结

创建方式 返回类型 是否需要 .value 适用场景
ref(value) Ref<T> ✅ 是 基本类型、单个值
reactive(obj) T(Proxy 代理的对象) ❌ 否 对象、数组等复杂结构
shallowRef() Ref<T>(浅层) ✅ 是 大型对象性能优化
shallowReactive() T(浅层 Proxy) ❌ 否 仅需顶层响应式

所以:只有通过 ref()(及其变体如 computedshallowRef)创建的响应式变量才是 Ref 类型;而 reactive() 创建的是普通对象类型的响应式代理,不是 Ref


补充:computed 也是 Ref

TypeScript 复制代码
import { computed } from 'vue'
const doubled = computed(() => count.value * 2) // ComputedRef<number>(是 Ref 的子类型)

computed 返回的是 ComputedRef<T>,它继承自 Ref<T>,所以也可以用 .value 访问,并且 isRef(doubled)true


补充:在 Vue 3 中,props 是一个响应式对象,但有一些重要特性和限制。


核心特性总结

特性 说明
响应性 props 是响应式的,父组件更新时子组件会响应
可观测性 ✅ 可以使用 watchcomputed 监听props变化
可修改性 ❌ 子组件中不能直接修改props(单向数据流)
类型 实际是 ShallowReadonly<Reactive<T>> 类型

具体分析


1. 响应式特性

vue

javascript 复制代码
<!-- 父组件 -->
<template>
  <ChildComponent :count="count" />
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0) // 父组件响应式数据
</script>

<!-- 子组件 -->
<script setup>
const props = defineProps(['count'])

// props.count 是响应式的
watch(() => props.count, (newVal) => {
  console.log('props变化:', newVal) // 父组件更新count时会触发
})
</script>

2. 只读特性

vue

javascript 复制代码
<script setup>
const props = defineProps({
  count: Number
})

// ❌ 错误:不能直接修改
props.count = 10  // TypeScript报错,运行时警告

// ✅ 正确做法:通过emit通知父组件修改
const emit = defineEmits(['update:count'])
const updateCount = () => {
  emit('update:count', 10)  // 使用v-model或.sync模式
}
</script>

3. 类型特性

实际上,Vue 3 中的 props 类型是:

  • ShallowReadonly<Reactive<T>>

  • 浅层只读的响应式代理

  • 深层嵌套对象在某些情况下可以修改(但不推荐)

vue

javascript 复制代码
<script setup>
const props = defineProps({
  user: {
    type: Object,
    default: () => ({ name: '', age: 0 })
  }
})

// ❌ 不推荐但技术上可行(Vue 3.4+会有警告)
props.user.name = '新名字'  // 深层属性修改,但破坏了单向数据流

// 正确:如果需要本地修改,创建副本
const localUser = reactive({ ...props.user })
</script>

4. props 响应式原理

javascript 复制代码
// 概念上的实现(简化版)
function createPropsProxy(rawProps) {
  return new Proxy(reactive(rawProps), {
    set(target, key, value) {
      if (/* 在开发环境 */) {
        console.warn('不要直接修改props')
      }
      return false // 阻止修改
    },
    deleteProperty() {
      return false // 阻止删除
    }
  })
}

5. 特殊情况:解构 props

vue

javascript 复制代码
<script setup>
const props = defineProps(['title', 'count'])

// ❌ 解构会丢失响应性
const { title, count } = props
watch(count, () => {})  // 不会触发,因为count不是响应式的

// ✅ 保持响应性的解构方式
import { toRefs } from 'vue'
const { title, count } = toRefs(props)  // 转为refs
watch(count, () => {})  // 正常触发

// ✅ 组合式API中的解构
const { count } = defineProps(['count'])  // Vue 3.4+ 自动保持响应性
</script>

6. 与普通响应式对象的区别

对比项 props reactive() 对象
可修改性 只读 可读写
修改方式 通过 emit 直接赋值
深层修改警告 Vue 3.4+ 会有 无警告
设计目的 组件通信(输入) 组件内部状态管理

7. 最佳实践

vue

javascript 复制代码
<script setup>
// 1. 明确声明props类型
const props = defineProps({
  // 基本类型
  title: String,
  count: {
    type: Number,
    required: true,
    default: 0,
    validator: (value) => value >= 0
  },
  // 复杂类型
  items: {
    type: Array,
    default: () => []
  }
})

// 2. 需要本地修改时创建副本
const localData = reactive({
  ...props, // 浅拷贝
  items: [...props.items] // 深拷贝数组
})

// 3. 监听props变化
watch(
  () => props.count,
  (newCount, oldCount) => {
    console.log(`count从${oldCount}变为${newCount}`)
  }
)

// 4. 使用computed基于props派生数据
const formattedTitle = computed(() => 
  props.title ? props.title.toUpperCase() : '默认标题'
)
</script>

总结

  1. props 是响应式的:可以监听变化,在模板中自动更新

  2. props 是只读的 :不能直接修改,必须通过 emit 通知父组件

  3. Vue 3.4+ 增强:对深层修改props会有警告,强化单向数据流

  4. 推荐使用 defineProps:获得完整的TypeScript类型支持

  5. 解构注意 :使用 toRefs(props) 或在 Vue 3.4+ 中直接解构


这种设计确保了 单向数据流 原则,使组件间的数据流向更清晰、可预测。

相关推荐
起名时在学Aiifox13 小时前
Vue 3 响应式缓存策略:从页面状态追踪到智能数据管理
前端·vue.js·缓存
天若有情67313 小时前
校园二手交易系统实战开发全记录(vue+SpringBoot+MySQL)
vue.js·spring boot·mysql
计算机程序设计小李同学13 小时前
个人数据管理系统
java·vue.js·spring boot·后端·web安全
JosieBook14 小时前
【Vue】09 Vue技术——JavaScript 数据代理的实现与应用
前端·javascript·vue.js
Eason_Lou15 小时前
webstorm开发vue项目快捷跳转到vue文件
ide·vue.js·webstorm
起名时在学Aiifox15 小时前
前端文件下载功能深度解析:从基础实现到企业级方案
前端·vue.js·typescript
云上凯歌16 小时前
01 ruoyi-vue-pro框架架构剖析
前端·vue.js·架构
毕设十刻17 小时前
基于Vue的迅读网上书城22f4d(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
爱健身的小刘同学17 小时前
Vue 3 + Leaflet 地图可视化
前端·javascript·vue.js