Published on

译: 使用 Ref 与 Reactive哪个更好?

Authors
  • avatar
    Name
    Deng Hua
    Twitter

文章翻译至: Ref vs. Reactive — Which is Best?

目录

自 Composition API 首次发布以来,这一直是每个 Vue 开发人员心中的一个问题:

refreactive 之间有什么区别,哪个更好?

我的极其简短的答案是:尽可能默认使用 ref

这不是一个非常令人满意的答案,所以让我们花更多的时间来看看我认为 refreactive 更好的原因 - 以及为什么你不应该相信我:)。

文章大致如下,分为三幕:

  • 第 1 幕: refreactive 之间的差异

首先,我们将了解 refreactive 的差异。我会尽量避免在这一点评价上做出判断,这样你就可以看到它们的所有不同之处。

  • 第 2 幕: ref 与 reactive 辩论

接下来,我将列出 refreactive 的论点,给出它们的优点和缺点。此时,您将能够做出自己的决定。

  • 第 3 幕:为什么我更喜欢 ref

我在文章结束时陈述我自己的观点并分享我自己的做法。我还分享了 Vue 社区中其他人的讨论还有看法,因为一个人的意见的影响有限。

这不仅仅是“ref 与reactive”的讨论,我希望当我们探索这个问题时,您能获得更多知识,从而提高您对 Composition API 的理解!


第一幕:ref 和reactive 之间的区别

首先,我想花一些时间具体讨论 refreactive 有何不同,以及它们的不同用途。

我试图在这个列表中详尽无遗。当然,我可能遗漏了一些东西 - 如果您知道我没有包含的内容,请告诉我。

抛开这些,让我们看看这两种工具之间的一些差异。

.value

refreactive 之间最明显的区别是,reactive 悄悄地为对象添加了一些"魔法"(不需要使用value), 而ref 要求您使用 value 属性:

const reactiveObj = reactive({ hello: 'world' });
reactiveObj.hello = 'new world';

const refString = ref('world');
refString.value = 'new world';

当您看到 something.value 并且已经熟悉 ref 的工作原理时,一眼就很容易理解这是一个响应式值。对于reactiveObj,这未必能一眼看出。

这里有一些注意事项:

  1. 如果您还不了解 ref 是如何工作的,那么看到 .value 对您来说意义不大。事实上,对于刚接触 Composition API 的人来说, reactive 是一个更加直观的 API。

  2. 非响应式对象可能具有 value 属性。但因为这与 ref API 冲突,所以无论您是否喜欢使用 ref ,我都会认为这是一种反模式。

实际上是整个讨论区别的关键——我们稍后会讨论这个问题。

工具和语法糖

这里 ref 的主要缺点是我们必须在每个地方写出这些 .value 访问器。这会变得相当乏味!

幸运的是,我们有一些额外的工具可以帮助我们缓解这个问题:

  1. template 展开
  2. watch 展开
  3. Volar

在很多地方,Vue 都会为我们解开 ref ,所以我们甚至不需要添加 .value 。在模板中,我们只需使用 ref 的名称:

<template>
  <div>{{ myRef }}</div>
</template>

<script setup>
  const myRef = ref('Please put this on the screen');
</script>

当使用watch时,我们指定要跟踪的依赖项,我们可以直接使用 ref

import { watch, ref } from 'vue';

const myRef = ref('This might change!');
// Vue自动解包
watch(myRef, (newValue) => console.log(newValue));

最后,Volar VS Code Extension将为我们自动完成参考,在需要的地方添加 .value 。您可以在 Volar: Auto Complete Refs 下的设置中启用此功能:

默认情况下禁用它以降低 CPU 使用率。

ref 在内部使用 reactive

这是您可能没有意识到的有趣事情。

当您将对象(包括数组、Date等)与 ref 一起使用时,它实际上是在幕后调用 reactive

ref({}) ~= ref(reactive({}))
  1. 首先,创建一个ref涉及到调用 toReactive 以获取内部的value
  2. 当传递的值是对象时,toReactive才会调用reactive

重新分配值

多年来,Vue开发者一直被重新赋值时响应式系统的工作原理所困扰,尤其是对对象和数组时:

myReactiveArray = [1, 2, 3]; // myReactiveArray的变化不会被监听

由于反应性系统的工作方式,这是 Vue 2 的一个大问题。 Vue 3 已经基本解决了这个问题,但是当涉及到 reactiveref 时,我们仍在处理这个问题。

let myReactiveArray = reactive([1, 2, 3]);

watchEffect(() => console.log('log',myReactiveArray));

setTimeout(() => {
  myReactiveArray = [4, 5, 6];
}, 1000);

您会看到, reactive 值无法按照您的期望进行重新赋值:

这是因为对前一个对象的引用被对新对象的引用覆盖。

基于Proxy的反应性系统仅在我们访问对象的属性时才起作用

重新分配值不会触发反应系统。必须修改现有对象的属性。

这也适用于 refs,但是由于每个 ref 具有标准的 .value 属性:

const myReactiveArray = ref([1, 2, 3]);

watchEffect(() => console.log('log', myReactiveArray.value));

setTimeout(() => {
  myReactiveArray.value = [4, 5, 6];
}, 1000);

refreactive 都需要读取属性才能使用响应式,两者没有区别。

Template Refs

使用最简单形式的模板引用时,重新分配值也可能会导致一些问题:

<template>
  <div>
    <h1 ref="heading">This is my page</h1>
  </div>
</template>

在这种情况下,我们无法使用 reactive 对象:

const heading = reactive(null);
watchEffect(() => console.log(heading)); // "null"

当组件第一次实例化时,heading将为null,因为 heading 还没有值。但是当组件挂载后,它也不会触发。 因为heading对象成为了一个新对象,我们的watcher失去了它的跟踪。之前reactive对象的引用将被覆盖。

我们需要在这里使用 ref:

const heading = ref(null);
watchEffect(() => console.log(heading.value)); //  <h1>This is my page</h1>

这一次,当组件挂载后,它会打印出该元素。这是因为只有 ref 可以以这种方式重新赋值。

如果一定要使用reactive,需要一些额外的语法:

<template>
  <div>
    <h1 :ref="(el) => { heading.element = el }">This is my page</h1>
  </div>
</template>

使用反应对象上的 el 属性:

const heading = reactive({ el: null });
watchEffect(() => console.log(heading.el)); // <h1>This is my Page</h1>

Alex Vipond 写了一本很棒的书,介绍如何使用函数引用模式在 Vue 中创建高度可重用的组件(我对此了解很多)。它令人大开眼界,我从这本书中学到了很多东西,所以帮自己一个忙,在这里抓住它:重新思考 Vue 中的可重用性

解构value

reactive 对象解构值会破坏反应性,因为反应性来自对象本身,而不是您要获取的属性:

const myObj = reactive({ prop1: 'hello', prop2: 'world' });
const { prop1 } = myObj; // prop1将是一个字符串,失去响应性

您必须先使用 toRefs 将对象的所有属性转换为 ref,现在prop1将具有响应式。

const myObj = reactive({ prop1: 'hello', prop2: 'world' });
const { prop1 } = toRefs(myObj); // ObjectRefImpl{...}

以这种方式使用 toRefs 可以让我们在使用 script setup 时解构我们的 props,而不会失去反应性:

const { prop1, prop2 } = toRefs(defineProps({
  prop1: {
    type: String,
    required: true,
  },
  prop2: {
    type: String,
    default: 'World',
  },
}));

P.S. 我更推荐这样使用,不会丢失原来的props。

const props = defineProps({
  prop1: {
    type: String,
    required: true,
  },
  prop2: {
    type: String,
    default: 'World',
  },
});
const { prop1, prop2 } = toRefs(props);

组合 ref 和 reactive

一种有趣的模式是将 refreactive 组合在一起。

我们可以获取ref并将它们组合到一个 reactive 对象中:

const lettuce = ref(true);
const burger = reactive({
  lettuce,
});

watchEffect(() => console.log(burger.lettuce)); // true

watch(lettuce, () => console.log("lettuce has changed")); // "lettuce has changed"

setTimeout(() => {
  lettuce.value = false;
}, 500);

现在无论如何访问对象属性,它们都会反应性地更新与其“连接”的所有其他内容。

使用 Ref 和 Reactive 管理状态

reactive 的最佳用途之一是管理状态。

使用 reactive 对象,我们可以将状态组建成对象,而不是出现一堆ref

// bad
const firstName = ref('Michael');
const lastName = ref('Thiessen');
const website = ref('michaelnthiessen.com');
const twitter = ref('@MichaelThiessen');
// good
const michael = reactive({
  firstName: 'Michael',
  lastName: 'Thiessen',
  website: 'michaelnthiessen.com',
  twitter: '@MichaelThiessen',
});

传递单个对象比大量ref要容易得多,并且有助于保持代码的干净。

还有一个额外的好处是它更具可读性。当新人阅读这段代码时,他们立即知道单个 reactive 对象内的属性是彼此相关的 - 否则,为什么它们会在一起?

然而,对ref state的相关部分进行分组的更好的解决方案可能是创建一个简单的可组合项(Hook):

const michael = usePerson({
  firstName: 'Michael',
  lastName: 'Thiessen',
  website: 'michaelnthiessen.com',
  twitter: '@MichaelThiessen',
});

const { twitter } = michael;

我们还有一个额外的好处,那就是我们还可以将method与可组合项放在一起。因此状态更改和其他业务逻辑可以集中并且更易于管理。

包装 Non-Reactive 库和对象

在与 Eduardo 讨论这个问题时,他提到他唯一一次使用 reactive 是为了包装集合:

const set = reactive(new Set());

set.add('hello');
set.add('there');
set.add('hello');

setTimeout(() => {
  set.add('another one');
}, 2000);

因为 Vue 的反应性系统使用Proxy,所以这是一种获取现有对象并为其添加响应式的非常简单的方法。

从Options API 重构为Composition API

在重构组件以使用 Composition API 时,reactive 似乎也非常有用:

如果您选择Composition,从 Vue2 的过渡会容易得多,特别是如果您有很多选项要更新的话。你只需复制并粘贴它们就可以了,但如果我必须选择 - ref 就是这样: )

我自己还没有尝试过,但这确实有意义。在Options API 中没有类似 ref 的特性,但 reactive 的工作方式与 data 字段内的响应式属性非常相似。

在这里,我们有一个简单的组件,它使用 Options API 更新组件状态中的字段:

export default {
  data() {
    username: 'Michael',
    access: 'superuser',
    favouriteColour: 'blue',
  },
  methods: {
    updateUsername(username) {
      this.username = username;
    },
  }
};

使用 Composition API 实现此功能的最简单方法是使用 reactive 复制并粘贴所有内容:

setup() {
  const state = reactive({
    username: 'Michael',
    access: 'superuser',
    favouriteColour: 'blue',
  });

  updateUsername(username) {
    state.username = username;
  }

  return {
    updateUsername,
    ...toRefs(state), // 使用toRefs,将对象中每个属性转为Ref
  }
}

还需要确保在访问响应值时更改 this → state,如果需要调用 updateUsername 则将this删除。

现在它将和之前Options API一样正常工作了,如果您愿意的话,使用 ref 重构会容易得多。但这种方法的好处是它很简单(可能足够简单,可以使用 codemod 或类似的东西实现自动化?)。

They’re just different

看完所有这些例子后,我们应该很清楚,如果我们真的需要的话,我们可以只用 refreactive 编写完美的 Vue 代码。他们都是完善的。

当我们探讨 refreactive 之间的争论时,请记住这一点。

第二幕:ref vs reactive

在开始讨论之前,我确实要指出,这些都是构建应用程序的完全有效且有用的方法。

refreactive 都是有用的工具,我们可以选择自己要使用的工具。

争论主要集中在两个要点:

  1. .valueref 一起使用。

  2. 一致性

1. 将 .value 与 ref 一起使用

当您看到 something.value 并且已经熟悉 ref 的工作原理时,一眼就很容易理解这是一个响应式值。对于响应式对象,这不一定那么清楚。

someObject.property = 'New Value'; // 它会响应式更新吗?直接看并不知道

someRef.value = 'New Value'; // 它是个ref,会响应式更新

这里有一些注意事项:

  1. 如果您还不了解 ref 的工作原理,那么看到 .value 对您来说毫无意义。事实上,对于刚接触 Composition API 的人来说,reactive 可能是一个更直观的 API。

  2. 非响应式对象也可能具有 value 属性。但这与 ref API 冲突,实际开发中,无论您是否喜欢使用 ref,我都不推荐这么做。

这就是我们辩论的重点。

那些喜欢 ref 的人认为一眼看过去就能知道它是响应式的,而喜欢 reactive 的人则不这么认为。

事实上,开发人员更喜欢使用 reactive 的原因之一正是因为这种 .value 语法对他们来说并不直观。

因此,如果我们不能用它来确定谁是更好的,也许我们可以使用一致性这一维度的评价来帮助我们。

2. 一致性

我们都同意的一件事,一致和简单是最好的。

这种一致性的主题体现在两个层面上。

首先,与其为了确定使用哪个工具而苦恼于每一行代码,不如坚持使用一种工具,只在绝对必要时才切换。

我们希望我们的行动保持一致,因为一遍又一遍地使用相同的工具要容易得多。

但是那应该是什么工具呢?

这是一致性的第二层 —— 工具本身的一致性。

观点1:为什么 ref 更一致?

使用 ref 的论点是它可以在任何地方使用,而 reactive 不能。如果我们选择 reactive 作为我们的工具,我们仍然必须在很多地方使用 ref ,这无助于我们实现一致性。

反过来,单纯使用ref意味着我们可以在任何地方使用它,并且几乎完全忽略reactive。这使我们的代码更加一致。

观点2: 为什么 reactive 更一致?

支持这一reactive观点的论点也可以依据一致性。

这一论点是,ref 的行为可能会非常不一致,会根据上下文而改变。有时候你需要使用 .value,有时候则不需要(Template...)。这种不一致性很令人困惑,使得工作变得更加困难。

相反,选择 reactive 意味着我们拥有了一种无论在什么情况下都能以一致的方式工作的工具。

那么,哪个最好?

至此,您可能已经猜到我不会告诉您哪个是最好的。

当我开始写这篇文章时,我希望得到一个无懈可击的答案。但我越深入研究这个问题,我就越不确定自己的观点。

最终,您需要做出自己的选择。

正如我们在第一幕中所看到的,您可以在任何情况下使用 refreactive 。也许几年前 Vue 3 刚刚问世时,支持或反对的理由都比较充分。但慢慢地,工具和框架会改善。

所以只要选择你喜欢的就可以了,不用担心。

只要与您编写代码的方式保持一致即可。

第三幕:为什么我更喜欢 ref

最后,是时候我就这个问题分享一下我自己的立场了。

我已经陷入这个问题太深了,怀疑一切:),以至于我也没有充分的理由选择其中一个。

但我更喜欢使用 ref 而不是 reactive

Why?

尽管我不太愿意承认,但对我来说,处理反应性的原子单位比移动对象更合理。

社区的讨论

我已经给了你很多理由来说明为什么简单地默认 ref 是最好的方法。

也许你同意我的观点,或者你可能发现我的一些论点有点站不住脚。

但我并不是唯一有这种想法的人。似乎 Vue 社区中的许多人都支持尽可能使用 ref 的想法。

我想说社区中的“大多数”人都有这种感觉,但我没有做过任何真正的调查,也没有任何真实的数据来支持如此有力的说法。

然而,这里有一些来自社区的例子。

  • Eduardo(Pinia 等的创建者):

就我个人而言,我只在数组之外的集合中使用 reactive(映射、集合等)。其他所有情况我都使用 ref,并且我认为使用.value的缺点会随着 ref 语法糖而消失。

  • Nuxt 团队的负责人 Daniel Roe 对于 ref 与reactive 这么说:

我认为 ref 是一个合理的默认行为,但我可能总是使用 ref(对于单个值)和 reactive(用于状态的概念单元)。”

  • 马库斯·奥伯勒纳 (Markus Oberlehner) :

ref()可以用于各种场合,reactive()不能。比起小小的烦恼,我更喜欢一致性。

...

那么,你应该使用 ref 还是reactive?

这是您的决定,但我绝对建议在大多数情况下使用 ref

两者都是有用且必要的工具 - 如果不是,它们就不会包含在框架中。

End.