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>
中也会被自动解包。