计算属性
基本示例
模板中的内联表达式非常方便,但它们主要用于简单的操作。将太多逻辑放入模板会使模板变得臃肿且难以维护。例如,如果我们有一个包含嵌套数组的对象
js
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
并且我们想要根据作者是否已有书籍显示不同的消息
template
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
此时,模板变得有些杂乱。我们需要花点时间才能意识到它根据 author.books
执行计算。更重要的是,如果我们需要多次将此计算包含在模板中,我们可能不想重复自己。
这就是为什么对于包含响应式数据的复杂逻辑,建议使用 计算属性。以下是相同的示例,经过重构
vue
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// a computed ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
这里我们声明了一个计算属性 publishedBooksMessage
。computed()
函数期望传入一个 getter 函数,返回值是一个 计算引用。类似于普通的引用,你可以通过 publishedBooksMessage.value
访问计算结果。计算引用在模板中也会自动展开,所以你可以在模板表达式中直接引用它们而不需要 .value
。
计算属性会自动跟踪它的响应式依赖。Vue 知道 publishedBooksMessage
的计算依赖于 author.books
,因此当 author.books
发生变化时,它将更新任何依赖于 publishedBooksMessage
的绑定。
另请参阅:计算属性类型
计算属性缓存与方法的比较
你可能已经注意到,我们可以通过在表达式中调用一个方法来实现相同的结果
template
<p>{{ calculateBooksMessage() }}</p>
js
// in component
function calculateBooksMessage() {
return author.books.length > 0 ? 'Yes' : 'No'
}
与计算属性不同,我们可以将相同的函数定义为方法。对于最终结果,这两种方法实际上是完全相同的。然而,区别在于 计算属性是基于它们的响应式依赖进行缓存的。 计算属性只有在某些响应式依赖发生变化时才会重新评估。这意味着只要 author.books
没有变化,多次访问 publishedBooksMessage
将立即返回之前计算的结果,而不需要再次运行 getter 函数。
这也意味着以下计算属性永远不会更新,因为 Date.now()
不是一个响应式依赖
js
const now = computed(() => Date.now())
相比之下,方法调用将在每次重新渲染时始终运行函数。
为什么我们需要缓存?想象我们有一个昂贵的计算属性 list
,它需要遍历一个巨大的数组并进行大量的计算。然后我们可能有其他依赖于 list
的计算属性。如果没有缓存,我们可能需要执行 list
的 getter 比必要的次数多得多!在你不想缓存的情况下,可以使用方法调用。
可写计算属性
计算属性默认是只读的。如果你尝试为计算属性分配新值,你将收到一个运行时警告。在极少数需要“可写”计算属性的情况下,你可以通过提供 getter 和 setter 来创建一个
vue
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// Note: we are using destructuring assignment syntax here.
[firstName.value, lastName.value] = newValue.split(' ')
}
})
</script>
现在当运行 fullName.value = 'John Doe'
时,setter 将被调用,并且 firstName
和 lastName
将相应地更新。
获取上一个值
- 仅支持 3.4+
如果你需要,可以通过访问 getter 的第一个参数来获取计算属性返回的上一个值
vue
<script setup>
import { ref, computed } from 'vue'
const count = ref(2)
// This computed will return the value of count when it's less or equal to 3.
// When count is >=4, the last value that fulfilled our condition will be returned
// instead until count is less or equal to 3
const alwaysSmall = computed((previous) => {
if (count.value <= 3) {
return count.value;
}
return previous;
})
</script>
如果你正在使用可写计算
vue
<script setup>
import { ref, computed } from 'vue'
const count = ref(2)
const alwaysSmall = computed({
get(previous) {
if (count.value <= 3) {
return count.value;
}
return previous;
},
set(newValue) {
count.value = newValue * 2;
}
})
</script>
最佳实践
getter 应该无副作用
记住,计算属性函数应该只执行纯计算,并且不产生副作用非常重要。例如,不要修改其他状态、进行异步请求或在内联计算属性中修改DOM! 将计算属性视为基于其他值声明性地描述如何推导出一个值——其唯一责任应该是计算并返回该值。在指南的后面部分,我们将讨论如何使用观察者来执行对状态变化的响应。
避免修改计算值
计算属性返回的值是从衍生状态推导出来的。将其视为一个临时快照——每次源状态发生变化时,都会创建一个新的快照。修改快照是没有意义的,因此计算属性返回的值应被视为只读的,并且不得修改——相反,应该更新它所依赖的源状态来触发新的计算。