原文链接:

前言
Vue 3.0 发布至今已经大半年过去了,我从最初的 Option API 的思维转换成 Composition API 花了很长时间,在使用过程中也出现了很多问题。我们都知道 Ref 和 Reactive 都是定义响应式数据的方式,而我在初学的时候从网上的大部分博客只得出过一个结论:Ref 是定义基本类型数据,Reactive 是定义引用类型数据的,但随着后面的实践发现,其实并不是很严谨,于是我找了这么一篇文章,我觉得讲得很好,便有了今天的翻译。下面的原文翻译采用意译并非直译,如有错误,请诸君批评与指正。

原文翻译

在写这篇文章的时候,Vue 3的发布离我们越来越近了。我认为我最激动的是看看其他开发者如何拥抱和使用它。在过去的几个月中,尽管我有机会使用过Vue 3,但我知道并非每个人都如此。

Vue 3 最大的特点就是 Composition API。这提供了一种创建组件的替代方法,该方法与现有的 Option API 截然不同。我毫不犹豫地承认,当我第一次看到它时,我并没有理解它,但随着我更多地去使用它,我发现它开始变得有意义。虽然您不会使用 Composition API 重写整个应用程序,但可以让您思考如何进一步提高创建组件和编写功能的方式。

我最近在Vue 3上做了几场演讲,并且不断出现的一个问题是何时使用 Ref vs Reactive 来声明数据的响应式。我并没有一个很好的答复,所以在过去的几周中,我着手去回答这个问题,而这篇文章正是该研究的结果。

我还想指出,这是我自己的看法,请不要将其视为应采取的“方式”。除非有人告诉我使用Ref & Reactive更好的方式,否则我目前会一直采用下面的方式去使用它。对于任何新技术,我认为需要花费一些时间来弄清楚我们如何使用它,从而得出一些最佳实践。

在开始之前,我将假设您至少已经了解了Composition API。本文将重点介绍Ref vs Reactive,而不是 Composition API 的机制,如果您对这方面的深入教程感兴趣,请告诉我。

Vue 2 中的响应式

为了给本文提供一些背景信息,我想快速探索如何在Vue 2应用程序中创建响应式性数据。当您希望 Vue 跟踪数据更改时,需要在从 data 函数返回的对象内部声明该属性。

  1. <template>
  2. <h1>{{ title }}</h1>
  3. </template>
  4. <script>
  5. export default {
  6. data() {
  7. return {
  8. title: "Hello, Vue!"
  9. };
  10. }
  11. };
  12. </script>

在Vue 2内部,为了追踪每个数据的变化,它会观察每个属性,并且使用 Object.defineProperty() 去创建getters和setters。这是对 Vue 2 响应式数据的最基本的解释,但我知道这并不是“魔法”。您不能只在任何地方创建数据并期望 Vue 对其进行跟踪,您必须遵循在data()函数中对其进行定义的约定。

Ref vs Reactive

使用 Options API,定义响应式性数据时必须遵循一些规则,Composition API 也不例外。您不能只声明数据并期望 Vue 进行跟踪更改。在下面的示例中,我定义了一个title属性,并从 setup() 函数返回了该 title,并在模板中使用。

  1. <template>
  2. <h1>{{ title }}</h1>
  3. </template>
  4. <script>
  5. export default {
  6. setup() {
  7. let title = "Hello, Vue 3!";
  8. return { title };
  9. }
  10. };
  11. </script>

虽然能正常运行,但是 title 属性并不是响应式数据。这意味着,如果某些方法更改了这个title属性后,DOM并不能更新数据。举例来说,您想在5秒钟后更新title,那么以下操作将无效。

<template>
  <h1>{{ title }}</h1>
</template>

<script>
  export default {
    setup() {
      let title = "Hello, Vue 3!";

      setTimeout(() => {
        title = "THIS IS A NEW TITLE";
      }, 5000);

      return { title };
    }
  };
</script>

为了解决上面的示例,我们可以使用 import { ref } from 'vue' 并使用 ref() 将其标记为响应式数据。在Vue 3内部,Vue将创建一个Proxy代理对象。

<template>
  <h1>{{ title }}</h1>
</template>

<script>
  import { ref } from "vue";

  export default {
    setup() {
      const title = ref("Hello, Vue 3!");

      setTimeout(() => {
        // you might be asking yourself, what is this .value all about...
        // more about that soon
        title.value = "New Title";
      }, 5000);

      return { title };
    }
  };
</script>

我还想明确一点,当提到Ref vs Reactive时,我相信有两个场景:第一个就是当您像我们上面那样创建组件时,你需要定义响应式数据的时候,另外一个就是在创建组合式函数可以被复用的时候。在本文中,我将对每种情况进行说明。

Ref

如果要使原始数据类型具有响应式性,则 ref() 将是您的首选。同样,这不是银弹,但这是一个不错的出发点。如果需要复习,JavaScript中的七个原始数据类型是:

  • String
  • Number
  • BigInt
  • Boolean
  • Symbol
  • Null
  • Undefined ```javascript import { ref } from “vue”;

export default { setup() { const title = ref(“”); const one = ref(1); const isValid = ref(true); const foo = ref(null); } };

在前面的示例中,我们有一个名为`title`的字符串,因此 `ref()` 是声明响应式性数据的不错选择。如果您对我们在下面编写的代码有疑问,请不要担心,我也有同样的问题。
```javascript
import { ref } from "vue";

export default {
  setup() {
    const title = ref("Hello, Vue 3!");

    setTimeout(() => {
      title.value = "New Title";
    }, 5000);

    return { title };
  }
};

当原始值将要更改时,为什么要使用const声明?我们不应该在这里使用let吗?如果要使用console.log(title),则可能希望看到值 Hello,Vue 3 !,而是得到一个看起来像这样的对象:

{_isRef: true}
value: (...)
_isRef: true
get value: ƒ value()
set value: ƒ value(newVal)
__proto__: Object

ref() 函数接受一个内部值,并返回一个响应式性并且可变更的ref对象。ref对象具有指向内部值的单个属性.value。这意味着,如果要访问或更改值,则需要使用title.value。并且因为这是一个不会改变的对象,所以我决定将其声明为const

Ref 拆箱

您可能会问的下一个问题是“为什么我们不必在模板中引用.value”?

<template>
  <h1>{{ title }}</h1>
</template>

当 ref 作为渲染上下文(从setup()返回的对象)的属性返回并在模板中访问时,它会自动展开为内部值,无需在模板中附加 .value,这个过程其实也叫“拆箱”的过程。

计算属性的工作原理相同,因此如果需要在setup()方法中使用计算属性的值,则需要使用.value。

Reactive

当您要在原始值上定义响应式数据时,我们仅查看了使用ref()的一些示例,如果要创建响应式对象(引用类型)会怎样?在这种情况下,您仍然可以使用**ref()**,但是在内部只是调用reactive()函数,所以我将坚持使用**reactive()**

另一方面,reactive()将不适用于原始值,reactive()获取一个对象并返回原始对象的响应式代理。这等效于2.x的Vue.observable(),并已重命名以避免与RxJS observables混淆。

import { reactive } from "vue";

export default {
  setup() {
    const data = reactive({
      title: "Hello, Vue 3"
    });

    return { data };
  }
};

这里的最大区别是,当您要在模板中访问reactive()定义的数据时。您将需要在模板中引用data.title,而在前面的示例中,data 是一个包含名为 title 的属性的对象。

Ref vs Reactive in Components

根据目前为止讨论的所有内容,答案很简单,对吧?我们应该只将ref()用于基本类型数据,并将reactive()用于引用类型数据。当我开始构建组件时,情况并非总是如此,事实上文档说明:

The difference between using ref and reactive can be somewhat compared to how you would write standard JavaScript logic.(ref和reactive差别有点就像与你如何编写规范化的JS逻辑作对比)

我开始思考这一点,并得出以下结论。在示例中,我们看到了一个名为title的单个属性,它是一个String,使用ref()非常有意义。但随着我的应用程序开始变得复杂,我定义了以下属性:

export default {
  setup() {
    const title = ref("Hello, World!");
    const description = ref("");
    const content = ref("Hello world");
    const wordCount = computed(() => content.value.length);

    return { title, description, content, wordCount };
  }
};

在这种情况下,我会将它们全部放到一个对象中,并使用reactive()方法。

<template>
  <div class="page">
    <h1>{{ page.title }}</h1>
    <p>{{ page.wordCount }}</p>
  </div>
</template>

<script>
  import { ref, computed, reactive } from "vue";

  export default {
    setup() {
      const page = reactive({
        title: "Hello, World!",
        description: "",
        content: "Hello world",
        wordCount: computed(() => page.content.length)
      });

      return { page };
    }
  };
</script>

这就是我在组件中一直采用Ref vs Reactive的方式,但我希望收到您的答复,你在做类似的事情吗?这种方法是错误的吗?请在下面给我一些反馈。

创建组合式逻辑(可复用)

在组件中使用ref()reactive()都将创建响应式性数据,只要您了解如何在setup()方法和模板中访问该数据,就不会有任何问题。

当您开始编写可组合函数时,您需要了解它们之间的区别。我将使用RFC文档中的示例,因为它在解释副作用方面做得很好。

比如有个需求是创建一些逻辑,以跟踪用户的鼠标位置,并且还需要具有在需要此逻辑的任何组件中重用此逻辑的能力。现在您创建了一个组合式函数,该函数跟踪x和y坐标,然后将其返回给使用者。

import { ref, onMounted, onUnmounted } from "vue";

export function useMousePosition() {
  const x = ref(0);
  const y = ref(0);

  function update(e) {
    x.value = e.pageX;
    y.value = e.pageY;
  }

  onMounted(() => {
    window.addEventListener("mousemove", update);
  });

  onUnmounted(() => {
    window.removeEventListener("mousemove", update);
  });

  return { x, y };
}

如果要在组件中使用此逻辑,则可以调用这个组合式函数,对返回对象进行解构,然后将x和y坐标返回给模板使用。

<template>
  <h1>Use Mouse Demo</h1>
  <p>x: {{ x }} | y: {{ y }}</p>
</template>

<script>
  import { useMousePosition } from "./use/useMousePosition";

  export default {
    setup() {
      const { x, y } = useMousePosition();

      return { x, y };
    }
  };
</script>

上述代码运行没有任何问题,但是如果你想把 x,y 重构为一个position对象里面的属性时:

import { ref, onMounted, onUnmounted } from "vue";

export function useMousePosition() {
  const pos = {
    x: 0,
    y: 0
  };

  function update(e) {
    pos.x = e.pageX;
    pos.y = e.pageY;
  }

  // ...
}

这种方法的问题在于,组合式函数的调用者必须始终保持对返回对象的引用,以保持响应式。这意味着该对象不能被解构或展开

// consuming component
export default {
  setup() {
    // reactivity lost!
    const { x, y } = useMousePosition();
    return {
      x,
      y
    };

    // reactivity lost!
    return {
      ...useMousePosition()
    };

    // this is the only way to retain reactivity.
    // you must return `pos` as-is and reference x and y as `pos.x` and `pos.y`
    // in the template.
    return {
      pos: useMousePosition()
    };
  }

这并不意味着您不能使用响应式式。有一个toRefs()方法将响应式对象转换为普通对象,结果就是这个对象上的每个属性都是一个指向原始对象的响应式引用

function useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0
  });

  // ...
  return toRefs(pos);
}

// x & y are now refs!
const { x, y } = useMousePosition();

总结

当我第一次开始使用 Composition API 创建组件时,我很难理解何时需要 ref() 和何时需要 reactive()。上述所研究的案例可能会存在一些差错,但是希望有人告诉我一些更好的方式。我希望我能帮助您解决一些问题,并希望在下面听到您的反馈。感谢您的阅读,我一如既往的朋友…

译者总结

  • 使用Composition API需要在setup函数中使用,并且返回需要给模板使用的数据(可以了解一下script setup)
  • Vue 2创建内部响应式数据的方式是在 data()函数所返回的对象中定义,并且调用 Object.defineProperty() 为每个属性设置gettersetter来追踪数据变更。Vue 3内部是使用Proxy代理对象来实现数据的响应式。
  • ref()定义的响应式数据需要通过.value来访问,而在模板中会进行一个拆箱的操作,不需要手动通过.value来访问。reactive()函数返回的对象需要在模板里通过.操作符访问。
  • ref()可以为基本类型和引用类型值创建响应式数据,而为引用类型创建响应式数据时,内部还是调用了reactive()。而reactive()只能接收一个对象,我们可以把一些相关联的数据都放在这个对象里,可以提高代码的可读性。
  • 如果逻辑可以复用可以使用组合式函数,这样其他组件也可以使用这个逻辑。reactive()函数返回的对象如果被解构的话,里面的数据将会失去响应式,可以通过toRefs对象里面的每个属性转化成ref来使用