# 计算属性及监听器
## 计算属性
模板内表达式是非常方便的,但它们是为简单操作设计的。太多的逻辑会造成模板臃肿难以维护。例如,有一个嵌套数组:
~~~
Vue.createApp({
data() {
return {
author: {
name: '小明',
books: [
'Vue 2 - 高级手册',
'Vue 3 - 基础手册',
'Vue 4 - 神秘之旅'
]
}
}
}
})
~~~
在作者是否有```books```时想展示不同的内容:
~~~
<div id="computed-basics">
<p>他的出版物::</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
</div>
~~~
这种情况下模板不再简单可读,你需要仔细阅读几秒钟才能意思到展示内容是依赖于```author.books```。如果在模板中想包含这个计算多次会导致更多的问题。
这就是为什么响应式数据里含有复杂逻辑时就需要使用计算属性。
<br />
### 基础示例
~~~
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>vue demo4</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
<p>他的出版物:</p>
<span>{{ publishedBooksMessage }}
</div>
</body>
<script type="text/javascript">
const app = {
data() {
return {
author: {
name: '小明',
books: [
'Vue 2 - 高级手册',
'Vue 3 - 基础手册',
'Vue 4 - 神秘之旅'
]
}
}
},
computed: {
// 计算属性寄存器
publishedBooksMessage() {
return this.author.books.length>0? 'Yes':'No'
}
}
}
Vue.createApp(app).mount("#app")
</script>
</html>
~~~
我们定义了一个计算属性```publishedBooksMessage```。试着更改data中的```books```数组,```publishedBooksMessage```也会相应的发生改变。
你可以在模板中像普通属性一样绑定计算属性,Vue会知道```vm.publishedBooksMessage```依赖于```vm.author.books```,所以会连动变化。最妙的是我们已声明式的创建了它们之间的依赖关系:计算属性寄存器函数没有副作用,反而更加容易测试和理解。
### 计算属性缓存 VS 方法
你可能注意到了,我们可以通过方法来实现一样的功能:
~~~
<p>{{ calculateBooksMessage() }}
~~~
~~~
// 组件实例中
methods: {
calculateBooksMessage() {
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
~~~
计算属性和方法也确实得到了相同的结果。不同点是**计算属性缓存响应式依赖关系**。依赖数据源不变化,多次调用计算属性直接返回前面缓存的计算结果。就是说```author.books```没有发生变化,调用```publishedBooksMessage```会立即返回前面缓存的计算结果而不需要重新运行函数。
下面这个计算属性永远不会更新,因为```Date.now()```不是响应式依赖:
~~~
computed: {
now() {
return Date.now()
}
}
~~~
相对而言,调用方法不管有没有重新渲染都会重复执行。
<br/>
为什么要缓存计算属性?相像一下,有一个非常耗费资源的```list```属性,在计算属性中需要遍历这个大型数组,而且有其他的计算属性依赖于```list```,如果不缓存我们就会没有必要的反复耗费资源来读取```list```。如果不想缓存就用```方法```来代替。
### 计算属性 Setter
计算属性默认是只读的,但如果需要你也可以进行set:
~~~
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>vue demo6</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
</div>
</body>
<script type="text/javascript">
const app = {
data() {
return {
firstName : 'zhou',
lastName : 'yu'
}
},
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]
}
}
}
}
const vm = Vue.createApp(app).mount("#app")
// get
console.log(vm.fullName)
// set
vm.fullName = 'John Doe'
console.log(vm.fullName)
</script>
</html>
~~~
现在你执行```vm.fullName = 'John Doe'```,set方法会更新```vm.firstName```和```vm.lastName```。
## 监听器
计算属性适用于大部分案例,是时候定制一个监听器了。Vue提供了很多通用的途径通过```监听```观察数据的变化。这在执行异步和耗时操作时监听返回数据变化时非常有用:
```
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>vue demo7</title>
<script src="https://unpkg.com/vue@next"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
对/错提问:
<input v-model="question" />
<p>{{ answer }}</p>
<p v-if="question_img">
<img :src="question_img" />
</p>
</div>
</body>
<script type="text/javascript">
const app = {
data() {
return {
question: '',
answer: '问题一般以问号结束哦. ;-)',
question_img: '',
count: 0
}
},
watch: {
// 无论问题是否变化,这个函数都会执行
question(newQuestion, oldQuestion) {
console.log("执行" + (++this.count) + "次")
if (newQuestion.indexOf('?') > -1 || newQuestion.indexOf("?")>0) {
this.getAnswer()
}
}
},
methods: {
getAnswer() {
this.answer = '思考中...'
axios
.get('https://yesno.wtf/api')
.then(response => {
this.answer = response.data.answer
this.question_img = response.data.image
})
.catch(error => {
this.answer = '错误! 网络访问失败. ' + error
})
}
}
}
Vue.createApp(app).mount('#app')
</script>
</html>
```
输入一个问题,以问号结束,执行结果:

上面这个示例,使用```watch```选项演示了一个异步操作(网络访问API)。```computed```实现不了。
### 计算属性 VS 监听属性,
Vue提供了一个更加通用的方法实现当前活动实例的数据响应和监听:**监听属性**,如果有些数据依据其他数据的变化而变化,会导致滥用```watch``` -- 特别是你有AngularJS背景。但是,很多时候使用计算属性比使用```watch```回调更好。看下面的例子:
~~~
<div id="demo">{{ fullName }}</div>
~~~
~~~
const vm = Vue.createApp({
data() {
return {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
}
},
watch: {
firstName(val) {
this.fullName = val + ' ' + this.lastName
},
lastName(val) {
this.fullName = this.firstName + ' ' + val
}
}
}).mount('#demo')
~~~
上面这些代码是命令式且重复的,与计算属性比较下:
~~~
const vm = Vue.createApp({
data() {
return {
firstName: 'Foo',
lastName: 'Bar'
}
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
}).mount('#demo')
~~~
是不是好多了?