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() → 非响应式标记
选择指南
-
使用
reactive当处理对象/集合且需要深度响应 -
使用
ref当处理原始值或需要替换整个对象引用 -
使用
computed当值依赖其他响应式数据 -
使用
readonly当需要防止意外修改 -
使用
shallow*系列优化性能或集成外部库 -
使用
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>
用于创建基本类型 (如 number、string、boolean)或普通对象的响应式引用。
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:TypeScriptconsole.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:TypeScriptconsole.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> 中配合 defineModel、props 等
props是reactive对象 (不可变,类型为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()(及其变体如computed、shallowRef)创建的响应式变量才是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 是响应式的,父组件更新时子组件会响应 |
| 可观测性 | ✅ 可以使用 watch、computed 监听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>
总结
-
props是响应式的:可以监听变化,在模板中自动更新 -
props是只读的 :不能直接修改,必须通过emit通知父组件 -
Vue 3.4+ 增强:对深层修改props会有警告,强化单向数据流
-
推荐使用
defineProps:获得完整的TypeScript类型支持 -
解构注意 :使用
toRefs(props)或在 Vue 3.4+ 中直接解构
这种设计确保了 单向数据流 原则,使组件间的数据流向更清晰、可预测。