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> (但无法肯定)。尽管如此,我们还是建议避免这种模式,因为他不好理解。