ref、toRef 和 toRefs 之间有什么区别

What is the difference between ref, toRef and toRefs

我刚刚开始使用 Vue 3 和 Composition API。

我想知道 reftoReftoRefs 之间有什么区别?

Vue 3 ref

ref 是 Vue 3 中的一种反应机制。其思想是将非对象包装在 reactive 对象中:

Takes an inner value and returns a reactive and mutable ref object. The ref object has a single property .value that points to the inner value.

嗯..为什么?

在JavaScript(以及许多 OOP 语言)中,有两种变量:valuereference

值变量: 如果一个变量 x 包含一个像 10 这样的值,它就是一个 value 变量。如果您要将 x 复制到 y,它只是复制值。 x 的任何未来更改都不会更改 y

引用变量:但是如果x是一个对象(或数组),那么它就是一个引用变量。有了这些,y 的属性 do 会在 x 的属性发生变化时发生变化,因为它们都 refer同一个对象。 (因为复制的是引用,而不是对象本身。如果意外出现,请使用 vanilla JavaScript 进行测试,您会看到 x === y

由于 Vue 3 反应性依赖于 JavaScript proxies 来检测变量变化——并且由于代理需要引用变量——Vue 提供了 ref 来将您的值变量转换为引用变量。

(并且 Vue 会自动在模板中解包你的 refs,这是 ref 的一个额外好处,如果你将你的值变量包装在一个手动对象。)

reactive

如果您的原始变量是对象(或数组),则不需要 ref 包装,因为它已经是 reference 类型。它只需要 Vue 的 reactive 功能(ref 也有):

const state = reactive({
  foo: 1,
  bar: 2
})

但是此对象的 属性 可能包含值,例如数字 10。如果你把一个value属性复制到别处,又会出现上面的问题。 Vue 无法跟踪副本,因为它不是引用变量。这就是 toRef 有用的地方。

toRef

toRef 将单个 reactive 对象 属性 转换为 ref 保持与父对象 的连接:

const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')
/*
fooRef: Ref<number>,
*/

toRefs

toRefsall 属性转换为具有 refs:

属性的普通对象
const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

无反应

reactive 基于给定的 object 创建一个深度反应 proxy object ].代理对象看起来与给定的普通对象完全相同,但任何变化,无论它有多深,都将是反应性的 - 这包括所有类型的变化,包括 属性 添加和删除。重要的是 reactive 只能处理对象,不能处理基元。

例如,const state = reactive({foo: {bar: 1}})表示:

  • state.foo 是反应式的(可用于模板、计算和观察)
  • state.foo.bar 是被动的
  • state.bazstate.foo.bazstate.foo.bar.baz 也是反应性的,即使 baz 尚不存在。这可能看起来令人惊讶(尤其是当您开始挖掘 vue 中的反应性如何工作时)。 state.baz 是反应式的,我的意思是在你的 template/computed properties/watches 中,你可以按字面意思写 state.baz 并期望你的逻辑被执行当 state.baz 可用 时再次出现。事实上,即使你在你的模板中写了类似 {{ state.baz ? state.baz.qux : "default value" }} 的东西,它也会起作用。显示的最终字符串将反应性地反映 state.baz.qux.

之所以会发生这种情况,是因为 reactive 不仅创建了一个顶级代理对象,它还递归地将所有嵌套对象转换为反应代理,并且这个过程在运行时继续发生,即使对于创建的子对象也是如此在飞行中。每当对反应对象 进行 属性 访问尝试时,都会 在运行时持续发现和跟踪对反应对象属性的依赖性。考虑到这一点,您可以逐步计算出这个表达式 {{ state.baz ? state.baz.qux : "default value" }}

  1. 第一次计算时,表达式将读取 baz off state(换句话说,属性 state 属性 baz 上尝试访问)。作为一个代理对象,state 会记住你的表达式依赖于它的 属性 baz,即使 baz 还不存在。 关闭反应性 baz 由拥有 属性.
  2. state 对象提供
  3. 因为 state.baz returns undefined,表达式的计算结果为“默认值”而无需费心查看 state.baz.qux。本轮没有记录到state.baz.qux上的依赖,但是这样就好了。 因为如果不先变异 baz 就无法变异 qux
  4. 在您的代码中某处您为 state.baz 分配了一个值:state.baz = { qux: "hello" }。此突变符合 statebaz 属性 的突变,因此您的表达式将被安排重新评估。同时,分配给 state.baz 的是为 { qux: "hello" }
  5. 动态创建的 子代理
  6. 你的表达式被再次计算,这次 state.baz 不是 undefined 所以表达式前进到 state.baz.qux。返回“hello”,并从代理对象 state.baz 中记录对 qux 属性 的依赖。 这就是我所说的在运行时发现并记录依赖项的意思
  7. 一段时间后你改变state.baz.qux = "hi"。这是 qux 属性 的突变,因此您的表达式将被再次计算。

考虑到以上内容,您应该也能理解这一点:您可以将 state.foo 存储在一个单独的变量中:const foo = state.foo。反应性可以很好地解决您的变量 foofoo 指向与 state.foo 指向的同一事物 - 一个反应式代理对象。反应的力量来自代理对象。顺便说一句,const baz = state.baz 的工作方式不同,稍后会详细介绍。

但是,总有一些边缘情况需要注意:

  1. 嵌套代理的递归创建只有在存在嵌套对象时才会发生。如果给定的 属性 不存在,或者存在但不是对象,则无法在 属性 处创建代理。例如。这就是为什么反应性不会影响 const baz = state.baz 创建的 baz 变量,也不会影响 const bar = state.foo.barbar 变量。说清楚一点,意思是你可以在你的template/computed/watch中使用state.bazstate.foo.bar,但不能使用上面创建的bazbar
  2. 如果您将嵌套代理提取到一个变量,它会从其原始父级分离。举个例子可以更清楚地说明这一点。下面的第二个赋值 (state.foo = {bar: 3}) 不会破坏 foo 的反应性,但 state.foo 将是一个新的代理对象,而 foo 变量仍然指向原始代理对象.
const state = reactive({foo: {bar: 1}});
const foo = state.foo;

state.foo.bar = 2;
foo.bar === 2; // true, because foo and state.foo are the same

state.foo = {bar: 3};
foo.bar === 3; // false, foo.bar will still be 2  

reftoRef 解决了其中一些边缘情况。

ref

ref 几乎是 reactive 也适用于原语。我们仍然无法将 JS 原语转换为 Proxy 对象,因此 ref 总是将提供的参数 X 包装成形状为 {value: X} 的对象。 X 是否原始并不重要,“装箱”总是会发生。如果将一个对象提供给 refref 在装箱后会在内部调用 reactive,因此结果也是深度响应式的。实践中的主要区别在于,在使用 ref 时,您需要记住在 js 代码中调用 .value。在你的模板中你不必调用 .value 因为 Vue 会自动解包模板中的引用。

const count = ref(1);
const objCount = ref({count: 1});

count.value === 1; // true
objCount.value.count === 1; // true

toRef

toRef 用于将反应对象的 属性 转换为 ref。你可能想知道为什么这是必要的,因为反应对象已经是深度反应的了。 toRef 在这里处理 reactive 中提到的两个极端情况。总之,toRef 可以将反应对象的任何 属性 转换为链接到其原始父级的 ref。 属性可以是初始不存在的,也可以是原始值

在同一示例中,状态定义为 const state = reactive({foo: {bar: 1}}):

  • const foo = toRef(state, 'foo')const foo = state.foo 非常相似,但有两个区别:
    1. foo 是一个 ref 所以你需要在 js 中做 foo.value;
    2. foo 链接到其父级,因此重新分配 state.foo = {bar: 2} 将反映在 foo.value
  • const baz = toRef(state, 'baz') 现在可以使用了。

toRefs

toRefs 是一种实用方法,用于破坏反应对象并将其所有属性转换为 ref:

const state = reactive({...});
return {...state}; // will not work, destruction removes reactivity 
return toRefs(state); // works