ref与reactive的区别与选择

在Vue 3的组合式API中,refreactive是创建响应式数据的两个核心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.definePropertygetset
  • 对象类型:内部调用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) // 1

ref 的使用

  • 通过.value访问:count.value
  • 通过.value修改:count.value++
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1

4. 在模板中的使用

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**:在&lt;script setup&gt;computedwatch等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 = 31

5. 函数参数传递

  • 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. 性能优化建议

  • 避免过度响应式:只对需要响应式的数据使用refreactive
  • **合理使用shallowRefshallowReactive**:对于深层嵌套对象,如果只需要浅层响应式,可以使用这些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. 嵌套使用 refreactive

错误示例

const state = reactive({
  count: ref(0) // 不必要的嵌套
})

解决方案

const state = reactive({
  count: 0 // 直接使用值
})

七、总结

特性 reactive ref
适用类型 对象/数组 所有类型
内部实现 Proxy Object.defineProperty + Proxy
访问方式 直接访问属性 通过.value访问
模板使用 {{ state.count }} {{ count }}(自动解包)
解构支持 需配合toRefs 基本类型解构丢失响应式
对象替换 不支持直接替换 支持通过.value替换
组合式函数 不推荐 推荐使用

选择建议

  1. **优先使用 ref**:

    • 基本数据类型
    • 可能被整体替换的对象
    • 组合式函数中返回数据
    • 需要在模板中简化语法
  2. **使用 reactive**:

    • 复杂的嵌套对象结构
    • 不需要整体替换的对象
    • 希望保持传统对象访问方式
  3. 结合使用

    • 对于大型组件,可以同时使用refreactive
    • 使用toRefs在它们之间进行转换

八、练习题

  1. 基础练习

    • 创建一个使用reactive的用户对象,包含name、age和address属性
    • 创建一个使用ref的计数器,实现自增和自减功能
    • 在模板中展示这些数据
  2. 进阶练习

    • 编写一个组合式函数useCounter,返回一个计数器对象,包含count值和increment、decrement方法
    • 使用toRefsreactive对象解构为多个ref
    • 实现一个切换主题的功能,使用ref存储主题状态
  3. 性能优化练习

    • 使用shallowRef创建一个大型对象,只监听其浅层变化
    • 使用markRaw标记一个不需要响应式的第三方库实例
    • 比较不同响应式API的性能差异

九、扩展阅读

在下一集中,我们将学习"响应式数据解构与toRefs",深入探讨如何在Vue 3中优雅地处理响应式数据的解构问题。

« 上一篇 响应式原理:Proxy vs defineProperty 下一篇 » 响应式数据解构与toRefs