ref与reactive的区别与选择
在Vue 3的组合式API中,ref和reactive是创建响应式数据的两个核心API。虽然它们都能实现响应式数据,但在使用场景、内部实现和使用方式上存在着重要的区别。本集我们将深入探讨这两个API的区别,并提供清晰的使用指导。
一、基本概念回顾
1. reactive API
reactive是Vue 3中用于创建响应式对象的API,它基于Proxy实现:
import { reactive } from 'vue'
const state = reactive({
count: 0,
message: 'Hello Vue 3'
})2. ref API
ref用于创建响应式的基本类型值(如字符串、数字、布尔值)或对象:
import { ref } from 'vue'
const count = ref(0)
const message = ref('Hello Vue 3')
const user = ref({ name: 'Alice', age: 30 })二、核心区别
1. 适用数据类型
| API | 适用数据类型 | 特点 |
|---|---|---|
reactive |
仅对象和数组 | 自动递归响应式 |
ref |
所有数据类型 | 基本类型和对象都适用 |
2. 内部实现机制
reactive 的实现
- 基于ES6 Proxy实现
- 直接代理原始对象
- 仅对对象类型有效
- 自动处理嵌套对象
// 简化实现原理
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
// 依赖收集
track(target, key)
const value = target[key]
// 递归处理嵌套对象
return typeof value === 'object' && value !== null ? reactive(value) : value
},
set(target, key, value) {
// 更新值
target[key] = value
// 触发更新
trigger(target, key)
return true
}
})
}ref 的实现
- 基本类型:使用
Object.defineProperty的get和set - 对象类型:内部调用
reactive实现 - 通过
.value访问和修改值
// 简化实现原理
function ref(value) {
const refObj = {
get value() {
track(refObj, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObj, 'value')
}
}
// 如果是对象类型,内部调用reactive
if (typeof value === 'object' && value !== null) {
refObj.value = reactive(value)
}
return refObj
}3. 访问和修改方式
reactive 的使用
- 直接访问属性:
state.count - 直接修改属性:
state.count++
const state = reactive({ count: 0 })
console.log(state.count) // 0
state.count++
console.log(state.count) // 1ref 的使用
- 通过
.value访问:count.value - 通过
.value修改:count.value++
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 14. 在模板中的使用
reactive 在模板中
- 直接使用属性名:
{{ state.count }}
<template>
<div>{{ state.count }}</div>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
</script>ref 在模板中
- 自动解包,无需
.value:{{ count }}
<template>
<div>{{ count }}</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>5. 解构赋值的处理
reactive 的解构问题
- 直接解构会丢失响应式:
const state = reactive({ count: 0, message: 'Hello' })
const { count, message } = state
// 失去响应式,修改不会触发更新
count++ // 不会触发组件更新ref 的解构
- 基本类型解构会丢失响应式:
const count = ref(0)
const { value: countValue } = count
// 失去响应式
countValue++ // 不会触发组件更新- 但可以通过
toRefs解决reactive的解构问题:
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, message: 'Hello' })
const { count, message } = toRefs(state)
// 保持响应式
count.value++ // 会触发组件更新三、使用场景选择
1. 何时使用 reactive?
- 复杂对象和数组:当你有一个包含多个属性的复杂对象或数组时
- 对象整体替换:当你不需要整体替换对象,只修改其属性时
- 嵌套对象结构:当对象内部有多层嵌套结构时
- 符合直觉的访问方式:当你希望像使用普通JavaScript对象一样访问响应式数据时
// 推荐使用 reactive 的场景
const user = reactive({
name: 'Alice',
age: 30,
address: {
city: 'Beijing',
street: 'Main St',
zipCode: '100000'
},
hobbies: ['reading', 'coding', 'hiking']
})2. 何时使用 ref?
- 基本数据类型:当你需要响应式的字符串、数字、布尔值等基本类型时
- 独立的对象:当你有一个可能需要被整体替换的对象时
- 在组合式函数中返回数据:当你编写自定义Composable函数时
- 在模板中使用:当你希望在模板中自动解包,简化语法时
// 推荐使用 ref 的场景
const count = ref(0)
const isLoading = ref(false)
const message = ref('Hello Vue 3')
// 可能被整体替换的对象
const currentUser = ref(null) // 初始为null,后续可能赋值为完整用户对象四、最佳实践
1. 统一使用规范
组件内部状态:
- 复杂对象:使用
reactive - 单个值:使用
ref
- 复杂对象:使用
组合式函数:
- 推荐使用
ref,因为它能更好地处理各种数据类型,并且在返回时更方便
- 推荐使用
2. 避免不必要的嵌套
// 不推荐
const state = reactive({
count: ref(0), // 嵌套使用,增加复杂性
message: ref('Hello')
})
// 推荐
const state = reactive({
count: 0,
message: 'Hello'
})
// 或者
const count = ref(0)
const message = ref('Hello')3. 注意ref的自动解包限制
- 模板中自动解包:在模板中使用
ref时,Vue会自动解包,无需.value - **JavaScript中需要
.value**:在<script setup>、computed、watch等JavaScript上下文中,必须使用.value - 嵌套在响应式对象中:当
ref被嵌套在reactive对象中时,Vue会自动解包
const state = reactive({
count: ref(0) // 嵌套在reactive对象中
})
console.log(state.count) // 自动解包,输出0,无需.value
state.count++ // 自动解包,修改值4. 结合toRefs使用
当你需要将reactive对象的属性解构出来,同时保持响应式时,可以使用toRefs:
import { reactive, toRefs } from 'vue'
const user = reactive({
name: 'Alice',
age: 30
})
// 使用toRefs解构,保持响应式
const { name, age } = toRefs(user)
// 修改会触发更新
name.value = 'Bob'
age.value = 315. 函数参数传递
reactive对象:传递给函数时,保持响应式,修改会影响原始对象ref对象:传递给函数时,需要注意是传递.value还是整个ref对象
// 传递reactive对象
function updateUser(user) {
user.age++ // 会修改原始对象
}
// 传递ref对象
function updateCount(countRef) {
countRef.value++ // 会修改原始ref的值
}
function updateCountValue(count) {
count++ // 不会修改原始ref的值,因为传递的是值的副本
}五、性能考虑
1. reactive 的性能
- 基于Proxy实现,性能良好
- 对嵌套对象自动递归响应式,可能会有轻微的性能开销
- 适合中大型对象
2. ref 的性能
- 基本类型:性能极佳,因为使用简单的get/set
- 对象类型:内部调用
reactive,性能与reactive相当 - 适合大量独立的响应式值
3. 性能优化建议
- 避免过度响应式:只对需要响应式的数据使用
ref或reactive - **合理使用
shallowRef和shallowReactive**:对于深层嵌套对象,如果只需要浅层响应式,可以使用这些API - **使用
markRaw**:对于不需要响应式的对象,可以使用markRaw标记,避免不必要的代理
import { reactive, markRaw } from 'vue'
const state = reactive({
// 大型第三方库实例,不需要响应式
thirdPartyLib: markRaw(new ThirdPartyLib())
})六、常见错误与解决方案
1. 忘记使用 .value
错误示例:
const count = ref(0)
count++ // 错误:忘记使用.value解决方案:
const count = ref(0)
count.value++ // 正确:使用.value修改值2. 直接解构 reactive 对象
错误示例:
const state = reactive({ count: 0 })
const { count } = state
count++ // 失去响应式解决方案:
const state = reactive({ count: 0 })
const { count } = toRefs(state)
count.value++ // 保持响应式3. 整体替换 reactive 对象
错误示例:
const state = reactive({ count: 0 })
state = { count: 1 } // 错误:不能直接替换reactive对象解决方案:
// 方案1:使用ref
const state = ref({ count: 0 })
state.value = { count: 1 } // 正确
// 方案2:修改属性而非替换对象
const state = reactive({ count: 0 })
state.count = 1 // 正确4. 嵌套使用 ref 和 reactive
错误示例:
const state = reactive({
count: ref(0) // 不必要的嵌套
})解决方案:
const state = reactive({
count: 0 // 直接使用值
})七、总结
| 特性 | reactive |
ref |
|---|---|---|
| 适用类型 | 对象/数组 | 所有类型 |
| 内部实现 | Proxy | Object.defineProperty + Proxy |
| 访问方式 | 直接访问属性 | 通过.value访问 |
| 模板使用 | {{ state.count }} |
{{ count }}(自动解包) |
| 解构支持 | 需配合toRefs |
基本类型解构丢失响应式 |
| 对象替换 | 不支持直接替换 | 支持通过.value替换 |
| 组合式函数 | 不推荐 | 推荐使用 |
选择建议
**优先使用
ref**:- 基本数据类型
- 可能被整体替换的对象
- 组合式函数中返回数据
- 需要在模板中简化语法
**使用
reactive**:- 复杂的嵌套对象结构
- 不需要整体替换的对象
- 希望保持传统对象访问方式
结合使用:
- 对于大型组件,可以同时使用
ref和reactive - 使用
toRefs在它们之间进行转换
- 对于大型组件,可以同时使用
八、练习题
基础练习:
- 创建一个使用
reactive的用户对象,包含name、age和address属性 - 创建一个使用
ref的计数器,实现自增和自减功能 - 在模板中展示这些数据
- 创建一个使用
进阶练习:
- 编写一个组合式函数
useCounter,返回一个计数器对象,包含count值和increment、decrement方法 - 使用
toRefs将reactive对象解构为多个ref - 实现一个切换主题的功能,使用
ref存储主题状态
- 编写一个组合式函数
性能优化练习:
- 使用
shallowRef创建一个大型对象,只监听其浅层变化 - 使用
markRaw标记一个不需要响应式的第三方库实例 - 比较不同响应式API的性能差异
- 使用
九、扩展阅读
在下一集中,我们将学习"响应式数据解构与toRefs",深入探讨如何在Vue 3中优雅地处理响应式数据的解构问题。