Vue系列 | 计算属性和监听器

计算属性

  • 减少模板中的计算逻辑

我们来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body>
<div id="example">
{{ message.split('').reverse().join('') }}
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script type="text/javascript">
new Vue({
el:'#example',
data: {
message: 'Hello'
}
})
</script>
</body>

结果:

1
olleH

下面我们来改写一下,使用计算属性来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="example">
{{ reversedMessage }}
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:'#example',
data: {
message: 'Hello'
},
computed: {
// 一个计算属性的getter
reversedMessage: function () {
// `this`指的是vm实例
return this.message.split('').reverse().join('')
}
}
})
</script>
</body>

对比一下:
首先,模板中的写法精简,起到了减少模板中的数据逻辑的作用;
其次,如果模板更多的地方包括了这个反转消息,我们只需计算一次,而如果把算法逻辑写到模板里,每次渲染,每个使用的地方都需要重新计算一次,耗费性能。而计算属性里面只需要计算一次,直到message改变的时候,才会重新计算;
再次,很显然写到计算属性里的代码更容易维护。

计算属性 VS 方法

  • 可以缓存数据计算的结果

我们可以使用方法来完成同样的表达:

1
2
3
4
//HTML
<div id="example">
{{ reversedMessage() }}
</div>

1
2
3
4
5
6
7
// 在组件的JS
methods: {
reversedMessage: function () {
// `this`指的是vm实例
return this.message.split('').reverse().join('')
}
}

两个方式的执行结果相同,但是计算属性可以基于数据的响应依赖来缓存数据。这里有两个重点:

  1. 计算属性可以缓存计算结果;
  2. 只有响应式属性才能被缓存。

意思是说只有响应式数据被改变的时候才重新计算。这意味着如果message没有变化,访问reversedMessage将会立刻返回计算结果而不再执行reversedMessage function()函数。

对比之下,方法在渲染发生的时候总是会被执行。

为什么我们需要缓存呢?
如果我们有一个属性A,它要遍历一个很大的数组来执行很多的计算,然后又其他的属性依赖它的计算结果。如果有缓存的话,下次访问A的时候我们直接使用就好,无需执行多余的计算,这就是缓存的意义,所以在不需要缓存的情况下,我们可以使用方法method代替。

计算属性 VS 监听属性

watch 属性是Vue实例上面观察和响应数据改变的更为通用的方式。

  • 当一些数据的变化依赖另外其他一些数据的时候,使用computedwatch更好。来看一个例子:

首先看计算属性的实现:

1
2
3
4
// HTML
<div id="example">
{{ fullName }}
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
// JS
var vm = new Vue({
el:'#example',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
},
})

接下来我们使用watch实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var vm = new Vue({
el:'#example',
data: {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
},
watch: {
firstName: function (val) {
this.fullName = val + ' ' + this.lastName
},
lastName: function (val) {
this.fullName = this.firstName + ' ' + val
}
},
})

对比之下,哪个更好一目了然。

计算属性Setter

计算属性默认只提供了getter,如果需要你可以提供一个setter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script type="text/javascript">
var vm = new Vue({
el:'#example',
data: {
firstName: 'Foo',
lastName: 'Bar',
},
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
console.log('set function')
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
},
})
vm.fullName = 'John Doe'
console.log(vm.firstName)
console.log(vm.lastName)
</script>

现在执行vm.fullName = 'John Doe',setter会被调用来分别更新vm.firstNamevm.lastName

监听器用例

大部分情况使用计算属性是合适的,但当你想执行异步或耗时的操作去响应数据的改变时,watch比较合适:

1
2
3
4
5
6
7
8
// HTML
<div id="example">
<p>
Ask a yes/no question:
<input v-model="question">
</p>
<p>{{ answer }}</p>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:'#example',
data: {
question: '',
answer: '直到你问一个问题,我才能回答你'
},
watch: {
question: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
},
created: function () {
this.debouncedGetAnswer = _.debounce(this.getAnswer,500)
},
methods: {
getAnswer: function () {
if (this.question.indexOf('?') === -1) {
this.answer = 'Questions usually contain a question mark.'
return
}
this.answer = "Think..."
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
va.answer = 'Erorr!' + error
})
}
}
})
</script>

结果:
Alt text

以上,watch允许我们去执行一个异步的操作(访问API),设置执行操作的频率,并设置中间状态,直到我们得到答案。这个计算属性是做不到的。

watch 操作项

watch几乎可以接受任何类型:对象、数组、函数。
对象的键是要监听的表达式,值是相应的回调,值可以是方法名字符串,或包含其他操作项的对象,Vue将在实例化时为对象的每一个条目调用$watch()方法。
用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// HTML
<script type="text/javascript">
var vm = new Vue({
el:'#example',
data: {
a: 1,
b: 2,
c: 3,
d: 4,
e: {
f: {
g: 5
}
}
},
watch: {
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
// 字符串方法名
b: 'someMethod',
// 无论嵌套的深度为何,只要所监督的属性发生变化,回调将被调用
c: {
handler: function (val, oldVal) {
console.log('I am c')
},
deep: true
},
// 该回调函数将在进入watch对象后后第一个调用
d: {
handler: 'startObservationMethod',
immediate: true
},
e: [
'handle1',
function handle2 (val, oldVal) {
// ...
console.log('I am handle2')
},
{
handler: function handle3 (val, oldVal) {
// ...
console.log('I am handle3')
},
// ...
}
],
// 监听 vm.e.f的值: {g: 5}
'e.f': function (val, oldVal) {
console.log('g' +' ' + 'update')
}
},
methods: {
someMethod () {
console.log('I am someMethod')
},
startObservationMethod () {
console.log('I am startObservationMethod')
},
handle1 () {
console.log('I am handle1')
}
}
})
vm.a = 2
vm.b = 3
vm.c = 4
vm.e = 6
vm.g = 8
</script>

依次打印结果:
I am startObservationMethod
new: 2, old: 1
I am someMethod
I am c
I am handle1
I am handle2
I am handle3
g update


参考:Computed Properties and Watchers