在 Vue 开发中,理解响应式数据的概念以及如何正确使用不同的响应式 API 是非常重要的。本文将详细介绍 Vue 2 中的 data 选项、Vue 3 Composition API 中的 ref、reactive、toRefs 和 toRef 函数之间的区别和使用场景,同时也会介绍 computed 和 watch 的区别与使用场景。
基本概念
响应式数据(Reactive Data)指的是当数据发生变化时,使用该数据的界面会自动更新,而无需手动操作DOM。 响应式数据具有以下特征:
- 自动追踪变化:框架会自动监测数据的变化
- 依赖关系建立:框架知道哪些视图依赖哪些数据
- 自动更新视图:当数据变化时,相关视图会自动重新渲染
1. Vue 2 中的 data 选项
在 Vue 2 中,我们通常在组件的 data 选项中定义响应式数据。
1.1 基本用法
javascript
export default {
data() {
return {
message: 'Hello Vue!',
count: 0,
user: {
name: 'Alice',
age: 25
}
}
}
}
1.2 特点
- data 必须是一个函数,返回一个包含响应式数据的对象
- Vue 会递归地将 data 中的所有属性转换为 getter/setter
- 访问和修改数据时不需要使用
.value - 适用于 Options API
2. Vue 3 中的 ref
ref 是 Vue 3 Composition API 中的一个函数,用于创建一个响应式的引用。
2.1 基本用法
javascript
import { ref } from 'vue'
// 基本类型
const count = ref(0)
const message = ref('Hello Vue 3!')
// 对象类型
const user = ref({
name: 'Alice',
age: 25
})
// 访问值
console.log(count.value) // 0
console.log(user.value.name) // 'Alice'
// 修改值
count.value++
user.value.name = 'Bob'
2.2 特点
- 可以包装任何类型的值(基本类型、对象、数组等)
- 返回一个带有
.value属性的响应式引用对象 - 在模板中使用时,Vue 会自动解包,无需使用
.value - 适用于需要将基本类型变为响应式的场景
3. Vue 3 中的 reactive
reactive 是 Vue 3 Composition API 中的另一个重要函数,用于创建响应式对象。
3.1 基本用法
javascript
import { reactive } from 'vue'
const state = reactive({
count: 0,
message: 'Hello Vue 3!',
user: {
name: 'Alice',
age: 25
}
})
// 访问值
console.log(state.count) // 0
console.log(state.user.name) // 'Alice'
// 修改值
state.count++
state.user.name = 'Bob'
3.2 特点
- 只能用于对象和数组(不能用于基本类型)
- 返回原始对象的代理(Proxy)
- 不需要使用
.value访问属性 - 对对象进行深层响应式转换
4. 四者之间的详细对比
| 特性 | data (Vue 2) | ref | reactive | toRefs | toRef |
|---|---|---|---|---|---|
| Vue 版本 | Vue 2 | Vue 3 | Vue 3 | Vue 3 | Vue 3 |
| API 类型 | Options API | Composition API | Composition API | Composition API | Composition API |
| 数据类型支持 | 任意类型 | 任意类型 | 仅对象/数组 | 对象属性 | 单个对象属性 |
| 访问方式 | 直接访问 | .value 访问(模板中除外) | 直接访问 | .value 访问 | .value 访问 |
| 解构支持 | 不支持 | 支持(但失去响应性) | 支持(但失去响应性) | 支持(保持响应性) | 支持(保持响应性) |
| 自动解包 | 不适用 | 模板中自动解包 | 不适用 | 不适用 | 不适用 |
5. 使用场景推荐
5.1 何时使用 data
- 使用 Vue 2 项目时
- 使用 Options API 时
- 需要在组件选项中定义响应式数据时
5.2 何时使用 ref
- 需要将基本类型(字符串、数字、布尔值)变为响应式时
- 需要重新分配整个对象时
- 在模板中需要直接使用变量名(而非对象属性)时
javascript
// 适合使用 ref 的场景
const isVisible = ref(true)
const userName = ref('Alice')
// 切换整个对象
let user = ref({
name: 'Alice',
age: 25
})
user.value = {
name: 'Bob',
age: 30
}
5.3 何时使用 reactive
- 需要创建包含多个相关属性的响应式状态对象时
- 处理复杂的数据结构时
- 不需要重新分配整个对象,只需要修改对象属性时
javascript
// 适合使用 reactive 的场景
const state = reactive({
user: {
name: 'Alice',
age: 25
},
permissions: ['read', 'write'],
isLoggedIn: true
})
5.4 何时使用 toRefs 和 toRef
- 需要将 reactive 对象的属性解构为独立的响应式引用时
- 需要将 reactive 对象的部分属性传递给其他函数时
- 需要保持解构后属性的响应性时
javascript
// 适合使用 toRefs 的场景
const state = reactive({
count: 0,
name: 'Alice',
permissions: []
})
// 解构但仍保持响应性
const { count, name } = toRefs(state)
// 适合使用 toRef 的场景
const countRef = toRef(state, 'count')
const nameRef = toRef(state, 'name')
6. 注意事项与最佳实践
6.1 ref 的注意事项
- 在 JavaScript 中访问 ref 的值时必须使用
.value - 解构 ref 会失去响应性
javascript
const count = ref(0)
const { value } = count // 错误:失去了响应性
6.2 reactive 的注意事项
- 不能用于基本类型
- 替换 reactive 对象本身不会保持响应性
javascript
let state = reactive({ count: 0 })
// 错误:这将失去响应性
state = reactive({ count: 1 })
- 解构 reactive 对象会失去响应性
javascript
const state = reactive({ count: 0, name: 'Alice' })
const { count } = state // 错误:失去了响应性
6.3 最佳实践
- 基本类型用 ref,对象类型优先考虑 reactive
javascript
// 推荐
const count = ref(0)
const state = reactive({
users: [],
loading: false
})
- 组合使用 ref 和 reactive
javascript
import { ref, reactive } from 'vue'
// 复杂状态使用 reactive
const state = reactive({
users: [],
pagination: {
page: 1,
size: 10
}
})
// 简单状态使用 ref
const loading = ref(false)
const errorMessage = ref('')
- 使用 toRefs 解构 reactive 对象
javascript
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
name: 'Alice'
})
// 正确的解构方式
const { count, name } = toRefs(state)
- 使用 toRef 创建单个响应式引用
javascript
import { reactive, toRef } from 'vue'
const state = reactive({
count: 0,
name: 'Alice'
})
// 创建单个响应式引用
const countRef = toRef(state, 'count')
const nameRef = toRef(state, 'name')
// 修改原始属性会影响 toRef 创建的引用
state.count++
console.log(countRef.value) // 1
// 修改 toRef 创建的引用也会影响原始属性
countRef.value++
console.log(state.count) // 2
7. 实际项目中的应用示例
以下是在实际项目中使用这些 API 的示例:
7.1 使用 ref 处理表单状态
javascript
import { ref } from 'vue'
export default {
setup() {
const formData = ref({
username: '',
email: '',
password: ''
})
const isSubmitting = ref(false)
const errorMessage = ref('')
const handleSubmit = async () => {
isSubmitting.value = true
try {
// 提交表单逻辑
} catch (error) {
errorMessage.value = error.message
} finally {
isSubmitting.value = false
}
}
return {
formData,
isSubmitting,
errorMessage,
handleSubmit
}
}
}
7.2 使用 reactive 管理复杂状态
javascript
import { reactive, computed } from 'vue'
export default {
setup() {
const state = reactive({
users: [],
filters: {
name: '',
status: 'active'
},
pagination: {
page: 1,
size: 10,
total: 0
},
loading: false,
error: null
})
// 计算属性
const activeUsers = computed(() => {
return state.users.filter(user => user.status === 'active')
})
return {
state,
activeUsers
}
}
}
8. computed 与 watch 的区别
在 Vue 开发中,除了响应式数据外,我们还需要处理派生状态和副作用。这就需要用到 computed 和 watch。
8.1 computed(计算属性)
computed 用于声明式地描述依赖响应式数据的复杂逻辑,创建一个响应式的只读值。
基本用法
javascript
import { ref, computed } from 'vue'
const count = ref(0)
// 只读计算属性
const doubled = computed(() => count.value * 2)
// 可写的计算属性
const writableComputed = computed({
get: () => count.value * 2,
set: (val) => {
count.value = val / 2
}
})
console.log(doubled.value) // 0
count.value++
console.log(doubled.value) // 2
特点
- 基于响应式依赖进行缓存,只有依赖发生改变时才会重新计算
- 默认是只读的,但也可以创建可写的计算属性
- 适用于复杂的逻辑计算,避免在模板中放入太多逻辑
- 在模板中使用时会被自动解包,无需使用
.value
8.2 watch(侦听器)
watch 用于监听响应式数据的变化并在变化时执行副作用。
基本用法
javascript
import { ref, watch } from 'vue'
const count = ref(0)
const user = ref({
name: 'Alice',
age: 25
})
// 侦听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
// 侦听 getter 函数
watch(
() => user.value.name,
(newName, oldName) => {
console.log(`name changed from ${oldName} to ${newName}`)
}
)
// 侦听多个数据源
watch(
[count, () => user.value.name],
([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} -> ${newCount}`)
console.log(`name: ${oldName} -> ${newName}`)
}
)
// 深度侦听
watch(
user,
(newUser, oldUser) => {
console.log('user changed', newUser, oldUser)
},
{ deep: true }
)
特点
- 用于执行副作用,如 API 请求、手动 DOM 操作等
- 不会缓存,只要依赖发生变化就会执行
- 可以监听单个或多个数据源
- 支持深度监听、立即执行等选项
8.3 computed 与 watch 的区别对比
| 特性 | computed | watch |
|---|---|---|
| 主要用途 | 创建响应式只读值 | 执行副作用 |
| 缓存机制 | 有缓存,依赖不变时不重新计算 | 无缓存,每次变化都执行 |
| 返回值 | 返回一个不可变的响应式对象 | 无返回值(执行副作用) |
| 使用场景 | 复杂逻辑计算、数据格式化 | API 请求、异步操作、DOM 操作 |
| 执行时机 | 模板渲染时按需计算 | 数据变化时立即执行 |
10. 总结
选择使用哪种响应式 API 主要取决于以下因素:
- Vue 版本:Vue 2 使用 data,Vue 3 可以使用 ref、reactive、toRefs 和 toRef
- 数据类型:基本类型推荐使用 ref,对象类型可以使用 reactive
- 使用场景:需要频繁替换整个值时使用 ref,处理复杂嵌套对象时使用 reactive
- API 风格:Options API 使用 data,Composition API 使用 ref、reactive、toRefs 和 toRef
- 派生状态处理:需要缓存的派生值用 computed,需要执行副作用用 watch
- 解构需求:需要解构 reactive 对象属性并保持响应性时使用 toRefs 或 toRef
理解这些差异有助于我们在实际开发中做出更好的选择,写出更高效、更易维护的 Vue 代码。