UnwrapNestedRef<T>
學習 reactive() 的解包機制。
建議您在學習完 ref() 和 reactive() 之後再閱讀此章節。
老實說,這也許不是一個非常重要的主題,因為在大多數情況下,您的 IDE 會為您推算輸出類型;你可能甚至沒有注意到這個東西的存在。
若您覺得本章節的內容很讓人困惑,請放心地跳過它!即使您不知道這些內容,您也能過得很好!
範例
您是否曾經好奇如果我們對著某個含有 Ref<T> 屬性的簡單對象 (plain object) 使用 ref() 會發生什麼事?例如:
import { ref } from 'vue'
const dog = ref({
name: 'hello'
})
const we = ref({
have: {
a: {
dog,
}
}
})
要從 we 拿到 hello,我們會很自然的想到 we.value.have.a.dog.value.name,因為 we 和 dog 都是藉由 ref() 所宣告出來的變數,因此便創造了一個巢狀結構。
但是當您嘗試運行這一段程式碼時,您會得到一個錯誤,內容是 TypeError: Cannot read properties of undefined (reading 'name')。怎麼會這樣呢?
發生這種事情的原因是:
- 正如我們在
ref()還是reactive()中所提到的,ref()在內部使用了reactive()。 - 在
reactive()中其實有個內建的解包機制,進而導致了上面的錯誤。
因此想要從 we 拿到 hello,正確的方式是 we.value.have.a.dog.name,因為 we.value.have.a.dog 被 reactive() 給解包了。
在這個章節中,我們會嘗試說明這個藏在 reactive() 中的秘密解包機制是如何運作的。
什麼是 UnwrapNestedRef<T>?
UnwrapNestedRef<T> 是 reactive() 的回傳型別,它的名字其實已經解釋了它的用途—解包所有 T 中的巢狀 Ref。
下面的虛擬碼展示了 UnwrapNestedRef<T> 的簡化版定義(但還是挺複雜的);它和原始碼不完全相同,但是很接近了:
type UnwrapNestedRef<T> = (
if (T is Ref) {
return T
} else {
return UnwrapRef<T>
}
)
type UnwrapRef<T> = (
if (T is Ref) {
return T['value']
} else if (T is plain object) {
return { for key in T: UnwrapRef<T[key]> }
} else if (T is Array) {
return [for key in T: UnwrapRef<T[key]>]
} else {
return T
}
)
下面的虛擬碼展示了依照上面的型別所實踐的虛構函式:
const unwrapNestedRef = <T>(arg: T): UnwrapNestedRef<T> => {
if (arg is Ref) {
return arg
} else {
return unwrapRef(arg)
}
}
const unwrapRef = <T>(arg: T): UnwrapRef<T> => {
if (arg is Ref) {
return arg.value
} else if (arg is plain object) {
const result = {}
for (const key in arg) {
result[key] = unwrapRef(arg[key])
}
return result
} else if (arg is Array) {
return arg.map((item) => unwrapRef(item))
} else {
return arg
}
}
上面的虛擬碼已經替一切做了很好的總結了!花點時間慢慢閱讀和理解這些虛擬碼,希望它能讓您對 reactive() 中的解包機制有個不錯的理解!
下面我們將提到一些常見的案例,還有應該要注意的事項。
鍵值集合 (Collections)
儲存於映射 (Map) 和集合 (Set) 等鍵值集合類型中的 Ref<T> 不會被 reactive() 解包,但響應性依然存在。
部分響應式物件
在使用 Vue 3 時,您應該極力避免宣告部分響應式物件,因為他們通常是 bug 的來源,例如:
import { reactive } from 'reactive'
const user = {
name: 'hello',
friend: {
child: reactive({
name: 'world',
}),
},
}
在這個範例中,修改 user.friend.child 中的任何屬性都會造成元件重新渲染,修改其他屬性則不會。在這種情況下,使用 ref() 會比 reactive() 還要好一些,因為看見 .value 我們至少能猜測它大概是一個 Ref<T> (但無法肯定)。儘管如此,我們還是建議避免這種模式,因為他不好理解。