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 有一些局限性:
-
有限的值类型:它只能用于对象类型 (对象、数组和如
Map
、Set
这样的集合类型)。它不能持有如string
、number
或boolean
这样的原始类型。 -
不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 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.