你可能会好奇:为什么我们需要使用带有 .value
的 ref,而不是普通的变量?为了解释这一点,我们需要简单地讨论一下 Vue 的响应式系统是如何工作的。
当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。
在标准的 JavaScript 中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。
该 .value
属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。从概念上讲,你可以将 ref 看作是一个像这样的对象:
// 伪代码,不是真正的实现
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
另一个 ref 的好处是,与普通变量不同,你可以将 ref 传递给函数,同时保留对最新值和响应式连接的访问。当将复杂的逻辑重构为可重用的代码时,这将非常有用。
该响应性系统在vue官网深入响应式原理章节中有更详细的讨论。
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')
}
非原始值将通过 reactive() 转换为响应式代理,该函数将在后面讨论。
也可以通过shallow ref来放弃深层响应性。对于浅层 ref,只有 .value
的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。
当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
要等待 DOM 更新完成后再执行额外的代码,可以使用nextTick()全局 API:
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 现在 DOM 已经更新了
}
reactive()
还有另一种声明响应式状态的方式,即使用 reactive()
API。与将内部值包装在特殊对象中的 ref 不同,reactive()
将使对象本身具有响应性:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
值得注意的是,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()
的局限性有限的值类型:它只能用于对象类型 (对象、数组和如 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。
局限性问题: reactive
本身存在一些局限性,可能会在开发过程中引发一些问题。这需要额外的注意力和处理,否则可能对开发造成麻烦。
数据类型限制: reactive
声明的数据类型仅限于对象,而ref
则更加灵活,可以容纳任何数据类型。这使得ref
更适合一般的响应式状态的声明。
官方推荐: 官方文档强烈建议使用ref()
作为声明响应式状态的首选。这是因为ref
更简单、更直观,同时避免了reactive
可能引发的一些问题。
总的来说:除非有特定的需求需要使用reactive
,否则在大多数情况下更推荐使用ref()
。
reactive
和 ref
对比reactive |
ref |
---|---|
❌ 只支持对象和数组(引用数据类型) | ✅ 支持基本数据类型 + 引用数据类型 |
✅ 在 和 中无差别使用 |
❌ 在 和 使用方式不同(在 中要使用 .value ) |
❌ 重新分配一个新对象会丢失响应性 | ✅ 重新分配一个新对象不会失去响应 |
能直接访问属性 | 需要使用 .value 访问属性 |
❌ 将对象传入函数时,失去响应 | ✅ 传入函数时,不会失去响应 |
❌ 解构时会丢失响应性,需使用 toRefs |
❌ 解构对象时会丢失响应性,需使用 toRefs |
即:
ref
用于将基本类型的数据和引用数据类型(对象)转换为响应式数据,通过 .value
访问和修改。
reactive
用于将对象转换为响应式数据,可以直接访问和修改属性,适用于复杂的嵌套对象和数组。
reactive
有限的值类型reactive
只能声明引用数据类型(对象)let obj = reactive({
name: '小明',
age: 18
})
ref
既能声明基本数据类型,也能声明对象和数组Vue 提供了 ref()
方法,允许我们创建可以使用任何值类型的响应式 ref
。
// 对象
const state = ref({})
// 数组
const state2 = ref([])
使用 ref
,你可以灵活地声明基本数据类型、对象或数组,而不受像 reactive
那样只能处理引用数据类型的限制。这为开发提供了更大的灵活性,尤其是在处理不同类型的数据时。
reactive
使用不当会失去响应使用 reactive
时,如果不当使用,可能导致响应性失效,带来一些困扰。这可能让开发者在愉快编码的同时,突然发现某些操作失去了响应性,不明所以。因此,建议在不了解 reactive
失去响应的情况下慎用,而更推荐使用 ref
。
reactive
一个整个对象或 reactive
对象let state = reactive({ count: 0 })
// 这个赋值将导致 state 失去响应
state = { count: 1 }
reactive
对象
{{ state }}
在 nextTick
中给 state
赋值一个 reactive
的响应式对象,但是 DOM 并没有更新。
解决方法:
不要直接整个对象替换,一个个属性赋值
let state = reactive({ count: 0 })
// state = { count: 1 }
state.count = 1
使用 Object.assign
let state = reactive({ count: 0 })
// state = { count: 1 },state 不会失去响应
state = Object.assign(state, { count: 1 })
使用 ref
定义对象
let state = ref({ count: 0 })
state.value = { count: 1 }
reactive
对象的属性赋值给变量(断开连接/深拷贝)这种操作类似于深拷贝,不再共享同一内存地址,而是只是字面量的赋值,对该变量的赋值不会影响原来对象的属性值。
let state = reactive({ count: 0 })
// 赋值给 n,n 和 state.count 不再共享响应性连接
let n = state.count
// 不影响原始的 state
n++
console.log(state.count) // 0
解决方案:
避免将 reactive
对象的属性赋值给变量。
reactive
对象解构时直接解构会失去响应。
let state = reactive({ count: 0 })
// 普通解构,count 和 state.count 失去了响应性连接
let { count } = state
count++ // state.count 值依旧是 0
解决方案:
使用 toRefs
解构,解构后的属性是 ref
的响应式变量。
const state = reactive({ count: 0 })
// 使用 toRefs 解构,后的属性为 ref 的响应式变量
let { count } = toRefs(state)
count.value++ // state.count 值改变为 1
ref
一把梭推荐使用 ref
,总结原因如下:
reactive
有限的值类型:只能声明引用数据类型(对象/数组)。
reactive
在一些情况下会失去响应,这可能导致数据回显失去响应(数据改了,DOM 没更新)。
{{ state.a }}
{{ state.b }}
{{ state.c }}
上面这个例子如果是使用 ref
进行声明,直接赋值即可,不需要将属性拆分一个个赋值。
使用 ref
替代 reactive
:
{{ state.a }}
{{ state.b }}
{{ state.c }}
给响应式对象的字面量赋一整个普通对象或 reactive
对象将导致 reactive
声明的响应式数据失去响应。
ref
适用范围更广,可声明基本数据类型和引用数据类型。
虽然使用 ref
声明的变量在读取和修改时都需要加 .value
小尾巴,但正因为有这个小尾巴,我们在 review 代码的时候就很清楚知道这是一个 ref
声明的响应式数据。
ref
的 .value
好麻烦!ref
声明的响应式变量携带迷人的 .value
小尾巴,让我们一眼就能确定它是一个响应式变量。虽然使用 ref
声明的变量在读取和修改时都需要加 .value
小尾巴,但是正因为有这个小尾巴,我们在 review 代码的时候就很清楚知道这是一个 ref
声明的响应式数据。
.value
推荐 ref
一把梭,但是 ref
又得到处 .value
,那就交给插件来完成吧!
Volar
自动补全 .value
(不是默认开启,需要手动开启)
reactive
重新赋值丢失响应是因为引用地址变了,被proxy
代理的对象已经不是原来的那个,所以丢失响应了。其实ref
也是一样的,当把.value
那一层替换成另外一个有着.value
的对象也会丢失响应。ref
定义的属性等价于reactive({ value: xxx })
。另外,说使用
Object.assign
为什么可以更新模板:
Object.assign
解释是这样的:如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。那个解决方法里不用重新赋值,直接
Object.assign(state, { count: 1 })
即可,所以只要proxy
代理的引用地址没变,就会一直存在响应性