你和我一样纠结过vue3的 ref() 和 reactive()吗?

1. 前言

看了挺多文章,大概都是说,ref用于包装基本数据类型(如数字、字符串),而reactive用于包装对象。

例如这样的:

或者这样的

又或者是这样的

2. 先看简单对比

ref()reactive()
✅支持基本数据类型+引用数据类型❌只支持对象和数组(引用数据类型)
❌在 <script><template> 使用方式不同(script中要.value)✅在 <script><template> 中无差别使用
✅重新分配一个新对象不会失去响应❌重新分配一个新对象会丢失响应性
需要使用 .value 访问属性能直接访问属性
✅传入函数时,不会失去响应❌将对象传入函数时,失去响应
✅解构对象时会丢失响应性,需使用toRefs❌解构时会丢失响应性,需使用toRefs

3. ref()

在组合式 API 中,推荐使用 ref() 函数来声明响应式状态:

import { ref } from 'vue'

const count = ref(0)

ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回:

const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1
ref()的深层响应性

Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map

Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到:

import { ref } from 'vue'

const obj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // 以下都会按照期望工作
  obj.value.nested.count++
  obj.value.arr.push('baz')
}

4. reactive()

还有另一种声明响应式状态的方式,即使用 reactive() API。与将内部值包装在特殊对象中的 ref 不同,reactive() 将使对象本身具有响应性:

import { reactive } from 'vue'

const state = reactive({ count: 0 })

非原始值将通过 reactive() 转换为响应式代理,该函数将在后面讨论。

也可以通过 shallow ref 来放弃深层响应性。对于浅层 ref,只有 .value 的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。

Reactive Proxy vs. Original

值得注意的是,reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的:

const raw = {}
const proxy = reactive(raw)

// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false

只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是 仅使用你声明对象的代理版本

为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:

// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true

// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true

这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:

const proxy = reactive({})

const raw = {}
proxy.nested = raw

console.log(proxy.nested === raw) // false
reactive() 的局限性

reactive() API 有一些局限性:

  1. 有限的值类型:它只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型)。它不能持有如 stringnumber 或 boolean 这样的原始类型

  2. 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:

  let state = reactive({ count: 0 })

  // 上面的 ({ count: 0 }) 引用将不再被追踪
  // (响应性连接已丢失!)
  state = reactive({ count: 1 })
  1. 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
  const state = reactive({ count: 0 })

  // 当解构时,count 已经与 state.count 断开连接
  let { count } = state
  // 不会影响原始的 state
  count++

  // 该函数接收到的是一个普通的数字
  // 并且无法追踪 state.count 的变化
  // 我们必须传入整个对象以保持响应性
  callSomeFunction(state.count)

由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。

总结

写下这篇文章只是为了解答自己的疑惑,也可能是强迫症犯了,非要弄清楚为什么有各种各样的说法。翻了许多文章,反复研究了几遍响应式基础,最终释然,不再纠结,自己爱怎么用就怎么用,但是对于那些 ref用于包装基本数据类型(如数字、字符串),而reactive用于包装对象。 的说法。自己心里也有个底。

  • ref() 除了需要使用 .value 访问比较麻烦外,几乎可以一把梭使用。
  • 当然,技术没有好坏,只是看你怎么使用。根据自己的开发习惯选择使用即可。
  • 既然作者给你提供了 reactive(),那也必然有某些场景需要用到,只是我还没遇到。
  • 也看到一些开发者说,为什么不统一用 ref(),然后把 .value去掉。对此,我一百个赞成。但是没办法,我们都不是造轮子的人,我们只是使用者,最终决定权还在那个男人手里。希望他能努力点!!!

官方文档建议:因为reactive()的局限性,所以建议使用 ref() 作为声明响应式状态的主要 API.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值