Ref<T> 和 ref()
這個章節也許是 Vue 3 最重要的一環!
如果您還沒有學過 TypeScript 或是物件導向設計,<T> 指的是 TypeScript 中的泛型。
什麼是 Ref<T>?
Ref<T> 是一個型別,它只有一個公開屬性 value。
簡單的 Ref<T> 介面如下:
interface Ref<T> {
value: T
}
一個 Ref<T> 只能存放一個任意型別的值,所以他可以是:
Ref<number>Ref<number[]>Ref<{ id: number, name: string }>Ref<Promise<() => void>>- ...任何你需要的型別!
什麼是 ref()?
ref() 是一個函式,只接收一個任意型別的參數;ref() 會把這個參數當做 Ref<T> 的 value 屬性值,然後回傳整個 Ref<T> 物件。
import { ref } from 'vue'
const name = ref('hello')
console.log(name) // { value: 'hello' }
要修改 Ref<T> 的 value,我們只需要使用典型的作法即可:
import { ref } from 'vue'
const name = ref('hello')
console.log(name.value) // 'hello'
name.value = 'world'
console.log(name.value) // 'world'
任何型別的 Ref<T> 都遵守同樣的規則,例如:
import { ref } from 'vue'
// 陣列
const fruits = ref(['apple', 'banana'])
console.log(fruits.value) // ['apple', 'banana']
fruits.value[0] = 'cherry'
console.log(fruits.value) // ['cherry', 'banana']
// 物件
const user = ref({
name: 'hello'
age: 5,
})
console.log(user.value) // { name: 'hello', age: 5 }
user.value.name = 'world'
console.log(user.value) // { name: 'world', age: 5 }
雖然 ref() 的回傳值看起來是一個長得像 { value: 'hello' } 的簡單對象 (plain object,或稱 POJO),事實上他不是!ref() 回傳的其實是一個叫做 RefImpl 的類別 (class) 實體 (instance),而且這個類別只有一個公開屬性 value。所以從使用者的角度來看 (你和我,開發人員),我們可以直接把 RefImpl 看做是 Ref<T>,因為他們有著相同的公開屬性。
此外,ref() 並不是盲目的把數值包成 Ref<T> 的結構而已,但是現在還不需要知道實際的邏輯。我們會在 ref() 還是 reactive() 章節做更詳細的描述。
很好,我們已經大致了解 Ref<T> 在 <script> 中運作的原理了。現在我們來看看 Ref<T> 如何在 <template> 中運作!
<template> 中的 Ref<T>
在 Vue 2,我們可以使用三種不同的語法在 <template> 中存取 <script> 的變數—雙大括弧 {{ }}、v-on (縮寫為 @) 和 v-bind (縮寫為 :)。這三種語法在 Vue 3 中仍然存在,但是邏輯上有小小的不同。以這個元件為例:
<template>
<!-- 這樣能正常運作嗎? -->
<div>{{ name.value }}</div>
</template>
<script setup>
import { ref } from 'vue'
const name = ref('hello')
</script>
由於 name 是一個 Ref<T>,我們會很合理的認為 <div>{{ name.value }}</div> 最後會得到 <div>hello</div>。但是當這個元件被渲染 (render) 後,輸出的 HTML 卻是 <div></div>,沒有中間的 hello — 我們的 hello 到哪去了?
在 Vue 3 中,當我們嘗試從 <template> 存取 Ref<T> 型別的變數時,有時候 (沒錯,有時候!) 他們會被自動解包。解包 (unwrap 或是 unref) 的意思是將 value 從 Ref<T> 中取出來。因此在某些情況下我們必須在 <template> 中省略 Ref<T> 後面的 .value,那麼「某些情況」指的是哪些情況呢?
規則很簡單:當該 Ref<T> 屬於 <script setup> 中的頂層屬性時,Vue 就會在 <template> 中將他自動解包;這個規則同樣適用於 v-on 和 v-bind。
所以在上方的例子中,如果我們想要在畫面上看見 hello,我們就必須寫 {{ name }} 而不是 {{ name.value }},因為 name 屬於 <script setup> 中的頂層屬性。
<template>
<!-- 這樣就能正常運作 -->
<div>{{ name }}</div>
</template>
<script setup>
import { ref } from 'vue'
const name = ref('hello')
</script>
我們再來看看一個自動解包的例子:
<template>
<div>
<h1>A: {{ age.toFixed }}</h1>
<h2>B: {{ user.age.toFixed }}</h2>
</div>
</template>
<script setup>
import { ref } from 'vue'
const age = ref(5)
const user = {
age: age,
}
</script>
這個元件所輸出的 HTML 會是這樣:
<div>
<h1>A: function toFixed() { [native code] }</h1>
<h2>B: </h2>
</div>
你知道為什麼會有這樣的差異嗎?
這是因為... (在看解答之前請先想想!)
age和user都是<script setup>中的頂層屬性。- 因為
age在<script setup>中是一個頂層的Ref<T>,他在<template>中會被自動解包,代表在<template>寫{{ age }}就會等於在<script setup>裡面寫age.value,因此得到5。 - 在 JavaScript 中,
toFixed是數字原型 (prototype) 中的一個方法;既然5是一個數字,那麼5.toFixed就會得到該方法,因此在畫面上就顯示了function toFixed() { [native code] }。 - 雖然
user.age和age在<script setup>的來源其實是同一個變數,但{{ user.age }}在<template>中不會被自動解包,因為user.age不是一個頂層屬性 —user才是! - 既然
user.age在<template>中沒有被自動解包,在<template>寫{{ user.age }}就會等於<script setup>中的user.age,也就是Ref<T>。 Ref<T>裡面沒有toFixed這個屬性,因此{{ user.age.toFixed }}就會是undefined,導致<h2>B: {{ undefined }}</h2>被渲染成<h2>B: </h2>。
太棒了,現在你知道 Ref<T> 在 <template> 中是如何運作的了!這個知識在使用組合式函式 (composable) 時尤其重要。若是不了解這些知識,我們的 <template> 最後就會出現一大堆本來可以被避免的 .value,造成程式碼的可讀性降低。
ComputedRef<T> 也屬於 Ref<T>
ComputedRef<T> 是 computed() 的回傳型別。
ComputedRef<T> 繼承自 Ref<T>,所以他們運作的邏輯很相似 — ComputedRef<T> 也只有一個公開屬性 value,當他處於 <script setup> 中的頂層時,在 <template> 中也會被自動解包。