Vue 3中的Ref() 与Reactive() - 什么是正确的选择?
Composition API有两种方法可以将反应状态引入组件中。因此,你将面临是否使用ref、reactive或两者的决定。我将帮助你做出正确的选择,但让我们先快速介绍一下这两种方式。
快速介绍
Ref()
和reactive()
是用来跟踪其参数的变化。当使用它们来初始化变量时,你给Vue的信息是:“嘿,我希望你在这些变量发生变化时,重新构建或重新评估依赖于这些变量的一切”。
在下面的示例中,personRef
和personReactive
都将在单击按钮后 更改其名称,但是person
只是一个普通的 JS 对象不会。
<template>
{{ person.name }} <!-- will NOT change to Amy -->
{{ personRef.name }} <!-- will change to Amy -->
{{ personReactive.name }} <!-- will change to Amy -->
<button @click="changeName('Amy')" />
</template>
<script>
const App = {
setup() {
const person = {name: 'John'};
const personRef = ref({name: 'John'});
const personReactive = reactive({name: 'John'});
const changeName = (name) => {
person.name = name;
personRef.value.name = name;
personReactive.name = name;
}
return {
changeName,
person,
personRef,
personReactive,
}
}
}
</script>
基本上,它们被用来使你的组件响应性(对更改做出反应)。
差异
有三个主要的区别,你应该注意到。
ref()
可以接受基本类型 作为参数(最常见的是:Boolean
、String
和Number
)以及对象,而reactive()
只能接受对象作为参数。
// INVALID
const x = reactive(true);
// VALID
const x = ref(true);
但是对于对象来说,两种语法都是有效的。
// VALID
const x = reactive({ name: 'John'});
// VALID
const x = ref({ name: 'John'});
ref()
有一个.value
的属性,你必须用它来获取它的内容,但用reactive()
你可以直接访问它。
// VALID
const x = reactive({ name: 'John'});
x.name = 'Ammy';
// x -> { name: 'Ammy' }
// VALID
const x = ref({ name: 'John'});
x.value.name = 'Ammy';
// x.value -> { name: 'Ammy' }
仅供参考:引用在传递给模板时会被解包,这就是为什么你不需要在那里写 .value
的原因。
- 使用
ref()
你可以替换一个对象的整个实例,但使用reactive()
你就不能了。
// INVALID - changes of x are NOT recorded by Vue
let x = reactive({name: 'John'});
x = reactive({todo: true});
// VALID
const x = ref({name: 'John'});
x.value = {todo: true};
为什么它们都存在呢?
看起来reactive()
只是ref()
的一个有限版本,那么你是否应该考虑在你的代码中使用reactive()
?为什么不总是使用ref()
呢?
我深挖了一下,找到了Vue 3团队决定给我们使用reactive()
的可能性的原因,而不是把它当作一个内部对象,对Vue程序员来说是不可用的。
而且…当reactive()
击败ref()
时,有一个用例。
转变视角
但在此之前,让我们看一下Vue.js的源代码,以便更好地理解反应性类之间的关系。这里你可以看到Vue.js 3中ref()
实现的部分内容。
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
在#8
行,有一个构造函数,我们每次写ref(x)
时都会调用这个构造函数。在它里面,有趣的事情发生了。在#10
行中,toReactive(x)
被调用,它是…只是一个函数,将一个对象改为reactive()
!
结论:ref()
是在使用reactive()
,你可以把ref()
看作(几乎):
const myRef = reactive({
value: "I'm ref!"
})
myRef.value // "I'm ref!"
myRef.value = "Changed"
myRef.value // "Changed"
什么时候reactive()比较好?
使用reactive()
的一个理由是为了避免模板。在使用 refs 时,你可能会因为在你的代码中到处写 .value
而感到厌烦。然而,如果你需要基本类型,你将无法逃避ref()
,除非你把它们放在一个对象中!
事实证明,当你想在一个变量中拥有一个完整的状态时,reactive()
很方便,就像在options API中做的那样(在data()
)。原因是reactive()是单独追踪每个属性的,而不是引用。
这意味着,当Vue试图找出是否应该更新依赖于它的东西时,reactive()
中的每个属性都被当作独立的ref()
。
如果你想使用ref()
作为一个状态容器,每当一个属性更新时,任何使用该状态的地方都会被更新。这将引发不必要的重读,并拖慢应用程序。
使用一个状态变量的例子。
const state = reactive({
person: {name: 'John'},
isLoading: false,
updated: true,
...
})
结论:你可以把reactive()
当作一个未被包裹的refs()
的容器,如果你想在一个组件内有一个单状态的变量,就可以使用它。
最后的说明
Ref()
和reactive()
在开始时可能看起来非常相似,但它们的目的却有些不同。在这个问题上没有好的编程实践,所以这完全取决于你和你的团队如何决定。
在我看来,你应该盲目地选择ref()
而不是reactive()
。代码会更加一致,你也不需要考虑使用哪一个— 编码会稍微快一点。用.value
访问变量可能会激怒你,但我认为那是一个变量是反应性的指标。用reactive()
我们就失去了这个信息,而且有必要检查变量定义(或内存)。
但请记住关于特殊的使用情况,使reactive()
成为你脑海中的好函数。如果你喜欢在一个组件内为一个状态设置一个变量,那么它恰恰是适合你的工具。
谢谢你!很高兴你发现它很有用
简洁而详细的解释;感谢你包括Vue.js源代码中的片段。
谢谢你!对于像我这样的初学者来说,这很容易理解。
解释得很好!我也是。
不错的总结。不过,有一个问题。你说"使用ref(),你可以替换一个对象的整个实例,但在使用reactive()时,你不能:"你的代码并没有真正证明这一点;它所证明的是你试图覆盖一个常量。这将会失败,但不是因为Vue的原因--试图覆盖一个常量显然是无效的JS。
谢谢你指出这一点!我会改正的。
很好的解释。在我的清单上有一段时间要深入研究这个话题,但我总是太忙或太懒,只是用ref() ,但不知道别人用reactive()。现在我确实明白了其中的区别。谢谢你的分享。