【譯】Vue 3 Composition API: Ref vs Reactive

前言
Vue 3.0發(fā)布至今已經(jīng)大半年過去了,我從最初的Option API的思維轉(zhuǎn)換成Composition API花了很長時(shí)間,在使用過程中也出現(xiàn)了很多問題。我們都知道Ref和Reactive都是定義響應(yīng)式數(shù)據(jù)的方式,而我在初學(xué)的時(shí)候從網(wǎng)上的大部分博客只得出過一個(gè)結(jié)論:Ref是定義基本類型數(shù)據(jù),Reactive是定義引用類型數(shù)據(jù)的,但隨著后面的實(shí)踐發(fā)現(xiàn),其實(shí)并不是很嚴(yán)謹(jǐn),于是我找了這么一篇文章,我覺得講得很好,便有了今天的翻譯。下面的原文翻譯采用意譯并非直譯,如有錯(cuò)誤,請諸君批評與指正。
原文翻譯
在寫這篇文章的時(shí)候,Vue 3的發(fā)布離我們越來越近了。我認(rèn)為我最激動的是看看其他開發(fā)者如何擁抱和使用它。在過去的幾個(gè)月中,盡管我有機(jī)會使用過Vue 3,但我知道并非每個(gè)人都如此。
Vue 3最大的特點(diǎn)就是Composition API。這提供了一種創(chuàng)建組件的替代方法,該方法與現(xiàn)有的Option API截然不同。我毫不猶豫地承認(rèn),當(dāng)我第一次看到它時(shí),我并沒有理解它,但隨著我更多地去使用它,我發(fā)現(xiàn)它開始變得有意義。雖然您不會使用Composition API重寫整個(gè)應(yīng)用程序,但可以讓您思考如何進(jìn)一步提高創(chuàng)建組件和編寫功能的方式。我最近在Vue 3上做了幾場演講,并且不斷出現(xiàn)的一個(gè)問題是何時(shí)使用Ref vs Reactive來聲明數(shù)據(jù)的響應(yīng)式。我并沒有一個(gè)很好的答復(fù),所以在過去的幾周中,我著手去回答這個(gè)問題,而這篇文章正是該研究的結(jié)果。
我還想指出,這是我自己的看法,請不要將其視為應(yīng)采取的“方式”。除非有人告訴我使用Ref & Reactive更好的方式,否則我目前會一直采用下面的方式去使用它。對于任何新技術(shù),我認(rèn)為需要花費(fèi)一些時(shí)間來弄清楚我們?nèi)绾问褂盟瑥亩贸鲆恍┳罴褜?shí)踐。在開始之前,我將假設(shè)您至少已經(jīng)了解了Composition API。本文將重點(diǎn)介紹Ref vs Reactive,而不是Composition API的機(jī)制,如果您對這方面的深入教程感興趣,請告訴我。
Vue 2 中的響應(yīng)式
為了給本文提供一些背景信息,我想快速探索如何在Vue 2應(yīng)用程序中創(chuàng)建響應(yīng)式性數(shù)據(jù)。當(dāng)您希望Vue跟蹤數(shù)據(jù)更改時(shí),需要在從data函數(shù)返回的對象內(nèi)部聲明該屬性。
<template>
<h1>{{ title }}</h1>
</template>
<script>
export default {
data() {
return {
title: "Hello, Vue!"
};
}
};
</script>
在Vue 2內(nèi)部,為了追蹤每個(gè)數(shù)據(jù)的變化,它會觀察每個(gè)屬性,并且使用Object.defineProperty()去創(chuàng)建getters和setters。這是對Vue 2響應(yīng)式數(shù)據(jù)的最基本的解釋,但我知道這并不是“魔法”。您不能只在任何地方創(chuàng)建數(shù)據(jù)并期望Vue對其進(jìn)行跟蹤,您必須遵循在data()函數(shù)中對其進(jìn)行定義的約定。
Ref vs Reactive
使用Options API,定義響應(yīng)式性數(shù)據(jù)時(shí)必須遵循一些規(guī)則,Composition API也不例外。您不能只聲明數(shù)據(jù)并期望Vue進(jìn)行跟蹤更改。在下面的示例中,我定義了一個(gè)title屬性,并從setup()函數(shù)返回了該title,并在模板中使用。
<template>
<h1>{{ title }}</h1>
</template>
<script>
export default {
setup() {
let title = "Hello, Vue 3!";
return { title };
}
};
</script>
雖然能正常運(yùn)行,但是title屬性并不是響應(yīng)式數(shù)據(jù)。這意味著,如果某些方法更改了這個(gè)title屬性后,DOM并不能更新數(shù)據(jù)。舉例來說,您想在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()將其標(biāo)記為響應(yīng)式數(shù)據(jù)。在Vue 3內(nèi)部,Vue將創(chuàng)建一個(gè)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>
我還想明確一點(diǎn),當(dāng)提到Ref vs Reactive時(shí),我相信有兩個(gè)場景:第一個(gè)就是當(dāng)您像我們上面那樣創(chuàng)建組件時(shí),你需要定義響應(yīng)式數(shù)據(jù)的時(shí)候,另外一個(gè)就是在創(chuàng)建組合式函數(shù)可以被復(fù)用的時(shí)候。在本文中,我將對每種情況進(jìn)行說明。
Ref
如果要使原始數(shù)據(jù)類型具有響應(yīng)式性,則ref()將是您的首選。同樣,這不是銀彈,但這是一個(gè)不錯(cuò)的出發(fā)點(diǎn)。如果需要復(fù)習(xí),JavaScript中的七個(gè)原始數(shù)據(jù)類型是:
String Number BigInt Boolean Symbol Null Undefined
import { ref } from "vue";
export default {
setup() {
const title = ref("");
const one = ref(1);
const isValid = ref(true);
const foo = ref(null);
}
};
在前面的示例中,我們有一個(gè)名為title的字符串,因此ref()是聲明響應(yīng)式性數(shù)據(jù)的不錯(cuò)選擇。如果您對我們在下面編寫的代碼有疑問,請不要擔(dān)心,我也有同樣的問題。
import { ref } from "vue";
export default {
setup() {
const title = ref("Hello, Vue 3!");
setTimeout(() => {
title.value = "New Title";
}, 5000);
return { title };
}
};
當(dāng)原始值將要更改時(shí),為什么要使用const聲明?我們不應(yīng)該在這里使用let嗎?如果要使用console.log(title),則可能希望看到值Hello,Vue 3 !,而是得到一個(gè)看起來像這樣的對象:
{_isRef: true}
value: (...)
_isRef: true
get value: ? value()
set value: ? value(newVal)
__proto__: Object
ref()函數(shù)接受一個(gè)內(nèi)部值,并返回一個(gè)響應(yīng)式性并且可變更的ref對象。ref對象具有指向內(nèi)部值的單個(gè)屬性.value。這意味著,如果要訪問或更改值,則需要使用title.value。并且因?yàn)檫@是一個(gè)不會改變的對象,所以我決定將其聲明為const。
Ref拆箱
您可能會問的下一個(gè)問題是“為什么我們不必在模板中引用.value”?
<template>
<h1>{{ title }}</h1>
</template>
當(dāng)ref作為渲染上下文(從setup()返回的對象)的屬性返回并在模板中訪問時(shí),它會自動展開為內(nèi)部值,無需在模板中附加.value,這個(gè)過程其實(shí)也叫“拆箱”的過程。
計(jì)算屬性的工作原理相同,因此如果需要在
setup()方法中使用計(jì)算屬性的值,則需要使用.value。
Reactive
當(dāng)您要在原始值上定義響應(yīng)式數(shù)據(jù)時(shí),我們僅查看了使用ref()的一些示例,如果要?jiǎng)?chuàng)建響應(yīng)式對象(引用類型)會怎樣?在這種情況下,您仍然可以使用ref(),但是在內(nèi)部只是調(diào)用reactive()函數(shù),所以我將堅(jiān)持使用reactive()。
另一方面,reactive()將不適用于原始值,reactive()獲取一個(gè)對象并返回原始對象的響應(yīng)式代理。這等效于2.x的Vue.observable(),并已重命名以避免與RxJS observables混淆。
import { reactive } from "vue";
export default {
setup() {
const data = reactive({
title: "Hello, Vue 3"
});
return { data };
}
};
這里的最大區(qū)別是,當(dāng)您要在模板中訪問reactive()定義的數(shù)據(jù)時(shí)。您將需要在模板中引用data.title,而在前面的示例中,data是一個(gè)包含名為title的屬性的對象。
Ref vs Reactive in Components
根據(jù)目前為止討論的所有內(nèi)容,答案很簡單,對吧?我們應(yīng)該只將ref()用于基本類型數(shù)據(jù),并將reactive()用于引用類型數(shù)據(jù)。當(dāng)我開始構(gòu)建組件時(shí),情況并非總是如此,事實(shí)上文檔說明:
The difference between using ref and reactive can be somewhat compared to how you would write standard JavaScript logic.(ref和reactive差別有點(diǎn)就像與你如何編寫規(guī)范化的JS邏輯作對比)
我開始思考這一點(diǎn),并得出以下結(jié)論。
在示例中,我們看到了一個(gè)名為title的單個(gè)屬性,它是一個(gè)String,使用ref()非常有意義。但隨著我的應(yīng)用程序開始變得復(fù)雜,我定義了以下屬性:
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 };
}
};
在這種情況下,我會將它們?nèi)糠诺揭粋€(gè)對象中,并使用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的方式,但我希望收到您的答復(fù),你在做類似的事情嗎?這種方法是錯(cuò)誤的嗎?請?jiān)谙旅娼o我一些反饋。
創(chuàng)建組合式邏輯(可復(fù)用)
在組件中使用ref()或reactive()都將創(chuàng)建響應(yīng)式性數(shù)據(jù),只要您了解如何在setup()方法和模板中訪問該數(shù)據(jù),就不會有任何問題。
當(dāng)您開始編寫可組合函數(shù)時(shí),您需要了解它們之間的區(qū)別。我將使用RFC文檔中的示例,因?yàn)樗诮忉尭弊饔梅矫孀龅煤芎谩?/p>
比如有個(gè)需求是創(chuàng)建一些邏輯,以跟蹤用戶的鼠標(biāo)位置,并且還需要具有在需要此邏輯的任何組件中重用此邏輯的能力?,F(xiàn)在您創(chuàng)建了一個(gè)組合式函數(shù),該函數(shù)跟蹤x和y坐標(biāo),然后將其返回給使用者。
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 };
}
如果要在組件中使用此邏輯,則可以調(diào)用這個(gè)組合式函數(shù),對返回對象進(jìn)行解構(gòu),然后將x和y坐標(biāo)返回給模板使用。
<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>
上述代碼運(yùn)行沒有任何問題,但是如果你想把x,y重構(gòu)為一個(gè)position對象里面的屬性時(shí):
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;
}
// ...
}
這種方法的問題在于,組合式函數(shù)的調(diào)用者必須始終保持對返回對象的引用,以保持響應(yīng)式。這意味著該對象不能被解構(gòu)或展開:
// 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()
};
}
};
這并不意味著您不能使用響應(yīng)式式。有一個(gè)toRefs()方法將響應(yīng)式對象轉(zhuǎn)換為普通對象,結(jié)果就是這個(gè)對象上的每個(gè)屬性都是一個(gè)指向原始對象的響應(yīng)式引用。
function useMousePosition() {
const pos = reactive({
x: 0,
y: 0
});
// ...
return toRefs(pos);
}
// x & y are now refs!
const { x, y } = useMousePosition();
總結(jié)
當(dāng)我第一次開始使用Composition API創(chuàng)建組件時(shí),我很難理解何時(shí)需要ref()和何時(shí)需要reactive()。上述所研究的案例可能會存在一些差錯(cuò),但是希望有人告訴我一些更好的方式。我希望我能幫助您解決一些問題,并希望在下面聽到您的反饋。感謝您的閱讀,我一如既往的朋友...
譯者總結(jié)
使用 Composition API需要在setup函數(shù)中使用,并且返回需要給模板使用的數(shù)據(jù)(可以了解一下script setup)Vue 2創(chuàng)建內(nèi)部響應(yīng)式數(shù)據(jù)的方式是在 data()函數(shù)所返回的對象中定義,并且調(diào)用Object.defineProperty()為每個(gè)屬性設(shè)置getter和setter來追蹤數(shù)據(jù)變更。Vue 3內(nèi)部是使用Proxy代理對象來實(shí)現(xiàn)數(shù)據(jù)的響應(yīng)式。ref()定義的響應(yīng)式數(shù)據(jù)需要通過.value來訪問,而在模板中會進(jìn)行一個(gè)拆箱的操作,不需要手動通過.value來訪問。reactive()函數(shù)返回的對象需要在模板里通過.操作符訪問。ref()可以為基本類型和引用類型值創(chuàng)建響應(yīng)式數(shù)據(jù),而為引用類型創(chuàng)建響應(yīng)式數(shù)據(jù)時(shí),內(nèi)部還是調(diào)用了reactive()。而reactive()只能接收一個(gè)對象,我們可以把一些相關(guān)聯(lián)的數(shù)據(jù)都放在這個(gè)對象里,可以提高代碼的可讀性。如果邏輯可以復(fù)用可以使用組合式函數(shù),這樣其他組件也可以使用這個(gè)邏輯。 reactive()函數(shù)返回的對象如果被解構(gòu)的話,里面的數(shù)據(jù)將會失去響應(yīng)式,可以通過toRefs把對象里面的每個(gè)屬性轉(zhuǎn)化成ref來使用。
原文鏈接
Vue 3 Composition API: Ref vs Reactive 鏈接:https://www.danvega.dev/blog/2020/02/12/vue3-ref-vs-reactive
The End
