vue的响应式ref和reactive

56 阅读4分钟

我们来详细讲解 Vue 3 中的两个核心响应式 API:ref 和 reactive。这是理解 Vue 3 组合式 API(Composition API)的基础。

核心概念

Vue 3 的响应式系统允许我们声明数据,并在数据变化时自动更新相关的视图。ref 和 reactive 是创建这种响应式数据的两种主要方式。


1. reactive

reactive() 用于创建一个响应式的对象(包括数组和内置的集合类型,如 Map、Set)。

用法

import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: {
    name: 'Alice',
    age: 25
  },
  hobbies: ['reading', 'gaming']
})

特点

  1. 针对对象:只对对象类型有效(对象、数组、Map、Set等),对原始类型(如 stringnumberboolean)无效。

  2. 深度响应reactive 会递归地将所有嵌套的属性也变为响应式的。上面例子中的 state.user.name 和 state.hobbies[0] 都是响应式的。

  3. 访问和修改:可以直接通过 . 来访问和修改属性,语法与普通对象无异。

    // 修改值
    state.count++
    state.user.name = 'Bob'
    state.hobbies.push('hiking')
    
    // 访问值
    console.log(state.count)
    
  4. 注意事项

    • 不能直接替换整个对象:如果你给 reactive 的变量赋予一个全新的对象,会失去响应性

      // 错误!这将导致 state 失去响应性
      state = reactive({ count: 10 })
      
      // 正确的方法是逐个修改属性
      Object.assign(state, { count: 10, someNewProp: 'hi' })
      
    • 与原始对象无关reactive 返回的是一个 Proxy 对象,它和原始对象是不相等的。

      const raw = {}
      const proxy = reactive(raw)
      console.log(proxy === raw) // false
      

2. ref

ref() 可以用来创建任何类型的响应式数据,包括原始类型。它通过一个 .value 属性来获取和设置值。

为什么需要 ref?

因为 reactive 只能用于对象,而原始值(如数字、字符串)需要有自己的响应式包装器,这就是 ref 的作用。

用法

import { ref } from 'vue'

const count = ref(0) // 原始类型
const user = ref({ name: 'Alice' }) // 对象类型
const list = ref([]) // 数组类型

特点

  1. 适用所有类型:既可以处理原始值,也可以处理对象。当处理对象时,内部会调用 reactive 来深度转换。

  2. 通过 .value 访问:你需要通过 .value 属性来读写 ref 的值。

    // 修改值
    count.value++
    user.value.name = 'Bob' // 对于对象,无需再 .value.name
    list.value.push('item')
    
    // 访问值
    console.log(count.value)
    console.log(user.value.name)
    
  3. 在模板中自动解包:当 ref 在模板顶层(<template>)被渲染时,会自动“解包”,无需使用 .value

    <template>
      <div>{{ count }}</div> <!-- 无需 .value -->
      <button @click="count++">Increment</button> <!-- 无需 .value -->
    </template>
    

    注意:在深层嵌套的对象或数组中,在模板中不会自动解包,仍需 .value

  4. 响应式替换:你可以将整个 .value 替换掉,它仍然是响应式的。

    user.value = { name: 'Charlie', age: 30 } // 完全没问题!
    

ref vs reactive:如何选择?

这是一个常见的初学者问题。以下是常见的实践和推荐用法:

特性refreactive
数据类型所有类型仅对象类型
访问方式需要 .value直接访问属性
模板使用自动解包(顶层)直接访问
重新赋值可以整体替换(.value = ...不能整体替换,会失去响应性

选择建议:

  1. 使用 ref 的情况

    • 定义原始类型的响应式数据(如 stringnumberboolean)。
    • 定义可能需要整体替换的响应式对象或数组。
    • 在不确定使用哪个时,优先使用 ref。因为它更通用,规则更一致(总是通过 .value 访问),减少了“何时需要 .value”的困惑。
  2. 使用 reactive 的情况

    • 定义不需要整体替换的、结构固定的复杂对象(如表单数据、页面状态对象)。
    • 当你确实想使用解构(但请注意会失去响应性,见下文)时,reactive 可以与 toRefs 配合。

一个重要的注意事项:解构

直接对 reactive 对象进行 ES6 解构,会破坏响应性,因为解出来的是普通变量。

const state = reactive({ count: 0 })
// 普通解构:count 不再是响应式的!
let { count } = state
count++ // 不会更新视图

// 使用 toRefs 保持响应性
import { toRefs } from 'vue'
const state = reactive({ count: 0 })
// 每个属性都被转换成了一个 ref
let { count } = toRefs(state)
count.value++ // 这样是响应式的

而 ref 本身就没有这个问题,因为它本身就是一个独立的变量。


总结

API推荐场景访问备注
ref通用首选。原始值、对象、数组。尤其是需要整体替换时。xxx.value模板中顶层自动解包,无需 .value
reactive不需要替换的复杂对象状态。直接 obj.key不能整体替换。解构需配合 toRefs

简单记忆:

  • 如果是单个变量(尤其是字符串、数字、布尔值),用 ref
  • 如果是一组逻辑相关的、需要一起使用的数据集合(如表单的多个字段),可以用 reactive 包装成一个对象,然后在组合式函数中返回时用 toRefs 转换为多个 ref,以便解构后保持响应性。

在实践中,很多开发者发现全程使用 ref 反而心智负担更小,因为规则始终如一(总是用 .value),所以这也是一个非常流行和有效的选择。