【譯】Vue 3 Composition API: Ref vs Reactive

前言

Vue 3.0 發佈至今已經大半年過去了,我從最初的Option API的思維轉換成Composition API花了很長時間,在使用過程中也出現了很多問題。我們都知道RefReactive都是定義響應式數據的方式,而我在初學的時候從網上的大部分博客只得出過一個結論: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函數返回的對象內部聲明該屬性。

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

<script>
  export default {
    data() {
      return {
        title: "Hello, Vue!"
      };
    }
  };
</script>

在 Vue 2 內部,爲了追蹤每個數據的變化,它會觀察每個屬性,並且使用Object.defineProperty()去創建 getters 和 setters。這是對 Vue 2 響應式數據的最基本的解釋,但我知道這並不是 “魔法”。您不能只在任何地方創建數據並期望 Vue 對其進行跟蹤,您必須遵循在data()函數中對其進行定義的約定。

Ref vs Reactive

使用 Options API,定義響應式性數據時必須遵循一些規則,Composition API 也不例外。您不能只聲明數據並期望 Vue 進行跟蹤更改。在下面的示例中,我定義了一個title屬性,並從setup()函數返回了該title,並在模板中使用。

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

<script>
  export default {
    setup() {
      let title = "Hello, Vue 3!";
      return { title };
    }
  };
</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 中的七個原始數據類型是:

import { ref } from "vue";

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

在前面的示例中,我們有一個名爲title的字符串,因此ref()是聲明響應式性數據的不錯選擇。如果您對我們在下面編寫的代碼有疑問,請不要擔心,我也有同樣的問題。

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()。上述所研究的案例可能會存在一些差錯,但是希望有人告訴我一些更好的方式。我希望我能幫助您解決一些問題,並希望在下面聽到您的反饋。感謝您的閱讀,我一如既往的朋友...

譯者總結

原文鏈接

關於本文

作者:不燒油的小火柴

https://juejin.cn/post/6949432566545907742

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Np68dO1wKwRZ33I-5XDh9Q