模板内的表达式计算是非常便利的,但是如果涉及到非常复杂的计算方式,一个结算结果如果依赖很多个变量,就会变得难以维护了,所以计算属性就此应运而生了。
使用计算属性(computed)有一个好处在于它有一个缓存机制,因此它不需要每次都重新计算。当其依赖属性的值发生变化时,这个属性的值会自动更新,与之相关的 DOM
部分也会同步自动更新。在处理一些复杂逻辑时计算属性是很有用的。
计算属性
在 Vue
中我们可以使用模板语法 {{}}
来展示一些数据,而当在模板中放入太多的逻辑会让模板过重且难以维护。这种情况下,Vue
给我们提供了一个特别好的解决方法,就是使用计算属性。我们可以将一些需要计算的过程写入到一个计算属性中去,然后让它动态的计算就可以了。
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如有一个嵌套数组对象:
Vue.createApp({
data() {
return {
author: {
name: '金庸',
books: [
'射雕英雄传',
'雪山飞狐',
'神雕侠侣'
]
}
}
}
})
我们想根据 author
是否已经有一些书来显示不同的消息:
<div id="computed-basics">
<p>出版过书籍吗?</p>
<span>{{ author.books.length > 0 ? '是' : '否' }}</span>
</div>
此时,模板不再是简单的和声明性的。必须先看一下它,然后才能意识到它执行的计算取决于 author.books
。如果要在模板中多次包含此计算,则问题会变得更糟。
所以,对于任何包含响应式数据的复杂逻辑,我们都应该使用计算属性。
基本例子
下面这个例子判断作者是否成功出版过书籍:
<div id="computed-basics">
<p>出版过书籍吗?</p>
<span>{{ publishedBooksMessage }}</span>
</div>
Vue.createApp({
data() {
return {
author: {
name: '金庸',
books: [
'射雕英雄传',
'雪山飞狐',
'神雕侠侣'
]
}
}
},
computed: {
// 计算属性的 getter
publishedBooksMessage() {
// this指向vm实例
return this.author.books.length > 0 ? '是' : '否'
}
}
}).mount('#computed-basics')
我们可以看一下浏览器中的效果:
上述代码中声明了一个计算属性 publishedBooksMessage
。尝试修改 data
中 books
数组的值,将可以看到 publishedBooksMessage
如何相应地更改。
我们可以像普通属性一样将数据绑定到模板中的计算属性。Vue
知道 vm.publishedBookMessage
依赖于 vm.author.books
,因此当 vm.author.books
发生改变时,所有依赖 vm.publishedBookMessage
绑定也会更新。而且最妙的是我们已经声明的方式创建了这个依赖关系:计算属性的 getter
函数没有副作用,这使得更易于测试和理解。
计算属性缓存 vs 方法
我们可以通过在表达式中调用方法来达到同样的效果:
<p>{{ calculateBooksMessage() }}</p>
// 在组件中
methods: {
calculateBooksMessage() {
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的反应依赖关系缓存的。计算属性只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 author.books
还没有发生改变,多次访问 publishedBookMessage
计算属性会立即返回之前的计算结果,而不必再次执行函数。
这也同样意味着下面的计算属性将不再更新,因为 Date.now()
不是响应式依赖:
computed: {
now() {
return Date.now()
}
}
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 list
,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 list
。如果没有缓存,我们将不可避免的多次执行 list
的 getter!如果不希望有缓存,可以使用 method
来替代。
计算属性的Setter
计算属性默认只有 getter
,不过在需要时你也可以提供一个 setter
:
// ...
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// ...
现在再运行 vm.fullName = 'John Doe'
时,setter
会被调用,vm.firstName
和 vm.lastName
也会相应地被更新。
监听属性
监听属性(watch)可以监听一个函数或者是一个变量,通过 watch
来响应数据的变化。虽然大多数情况计算属性都可以满足需要,但有时还是需要使用侦听器。当需要在数据发生变化时执行异步操作或者开销较大的操作时,就需要自定义监听器。
示例:
例如我们想实现一个计数器,我们可以通过 watch
来响应数据的变化:
<div id="increase">
<p>计算器:{{num}}</p>
<button @click="num++">点我加一</button>
</div>
const app = Vue.createApp({
data() {
return {
num: 1
}
}
}).mount('#increase')
// 监听器
app.$watch('num', function(navl, oval){
alert("计数器的值从 " + oval + "变为" + navl);
});
我们在浏览器中看一下演示效果:
watch
这个对象里面是一个函数,函数的名称是 data
中的属性名称,也就是 num
。并且 watch
中的函数不需要调用。
当属性 num
发生改变,就会触发 watch
函数(num
所对应的函数),每个函数都会接受两个值,一个是新值navl
,一个是旧值oval
。
例如当我们第一次点击按钮时,会弹出一个弹出层,告诉我们“计数器的值从 1 变为 2”,然后我们点击弹出层的 “确认” 按钮,计数器显示的值成功变为了 2。