本文译自Vue.js官方文档
【约定:下文中的 “计算属性” 即为 “computed属性” 】
背景简介
在模板内使用表达式是非常方便的,但也只限于简单的表达式而已,如果将过多的业务逻辑放在模板内处理,就会显得臃肿而难以维护。请看下列代码:
<div id="example">
{{ message.split( ' ' ).reverse().join( ' ' ) }}
</div>
由此可见这个模板不再那么简洁和表述清晰了,你需要花费好些时间才能看出它最终要显示什么样的结果。当你在模板中多次使用这样处理过的信息时结果将变得更糟。
这也就是为什么在碰到复杂逻辑运算时我们应该使用计算属性的原因。
基本用法示例
<div id="example">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
el: '#example',
data: {
message: ' Hello '
},
computed: {
// a computed getter
reversedMessage: function(){
// `this` 指向vm实例
return this.message.split( ' ' ).reverse().join( ' ' )
}
}
})
输出结果:
Original message: "Hello"
Computed reversed message: "olleh"
上面的代码中我们声明了一个计算属性reversedMessage
,这里其实只是声明了这个属性的一个 getter 方法,关于它的 setter 方法我们在本文的后面有讨论。
console.log( vm.reversedMessage ) // => 'olleH'
vm.message = 'Goodbye'
console.log( vm.reversedMessage ) // => 'eybdooG'
我们可以在控制台试着改变一下上例中的vm.message
的值,你会发现输出的vm.reversedMessage
的值也会立刻跟着改变。
我们可以在模板中使用计算属性来作数据绑定,跟使用一般的属性一个样,Vue知道这个计算属性vm.reversedMessage
依赖于数据属性vm.message
,所以当vm.message
的值变化时,vm.reversedMessage
的返回值会重新计算(也就是它的 *getter 方法会被调用),Vue将会更新所有的引用了vm.reversedMessage
的数据。由此可见我们通过声明了一个计算属性的 getter 方法,使这个计算属性与 getter 方法内使用到的其它属性产生了依赖关系,且这个方法不会产生副效应,使得我们更容易去测试和理解这个计算属性的工作过程。
计算属性的缓存 vs 方法
我们可能会想到通过在模板中调用方法也能达到相同的效果:
<p>Reversed message: "{{ reverseMessage() }}"</p>
// 此处省略其它代码
methods: {
reverseMessage: function() {
return this.message.split( ' ' ).reverse().join( ' ' )
}
}
我们在methods内定义了一个同样的方法来替代计算属性。其实使用这两种方式所达到的最终结果完全一样,唯一的区别是计算属性可以基于它们的响应式依赖(代表它所依赖的某些属性或数据是响应式的)对计算结果作缓存,只有它的响应式依赖项message
的值发生改变时,这个计算属性才会重新计算,也就是说只要message
的值没有发生改变,所有引用reversedMessage
计算属性的地方将立刻返回先前的计算值,而不会再去调用那个 getter 方法重新计算值。这意味着下列代码中的计算属性now
的值永远都不会更新,因为Date.now()
不是响应式的:
computed: {
now: function() {
return Date.now()
}
}
相比而言,通过在模板内调用方法就不会缓存,只要页面重新渲染, getter 方法就会被执行。
我们为什么需要缓存呢?假设我们有一个开销巨大的计算属性A,A需要遍历一个巨大的数组而且还要做大量计算,然后我们还有另外的一些计算属性且它们依赖于A。如果没有缓存的话,不管A的值有没有发生改变,A的值都要计算多次(A的 getter 被执行多次);如果有缓存的话,在A没有发生改变的情况下,那些依赖于A的计算属性可以使用同一个A的缓存值。
计算属性 vs watch
Vue明确的提供了一个通用的手段来发现数据的变化并为之做出响应,这就是使用watch
选项。当一些数据需要根据其它数据的变化做出相应的改变时,使用watch
确实是一种诱人的办法,尤其对于那些很熟悉AngularJS的同学更是如此。不过在这里我们还是更提倡使用计算属性而不是watch
,看看下面的例子就能区分它们的优劣了:
<div id="demo">{{ fullName }}</div>
var vm = new Vue({
el: '#demo',
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
}
}
})
上面的代码显得很繁琐,但又是不可避免的。再看看下列使用计算属性实现的代码:
var vm = new Vue( {
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
} )
显而易见,使用计算属性的代码简洁多了!
通过以上的了解,我们知道在大多数情况下使用计算属性是更合适的,然而有个时候我们确实需要自定义watcher来满足某些需求,这也是Vue提供了一个watch
选项的原因。假设有这样一个需求:我们要对数据的改变做出异步响应或做些消耗巨大的操作。请看下列代码:
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question">
</p>
<p>{{ answer }}</p>
</div>
<script src="https://cdn.jsdeliver.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>
var watchExampleVM = new Vue({
el: '#watch-example',
data: {
question: ' ',
answer: 'I cannot give you an answer until you ask a question!'
},
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 = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api').then(function (response) {
vm.answer = _.capitalize(response.data.answer)
}).catch(function (error) {
vm.answer = 'Error! Could not reach the API. ' + error
})
}
}
})
</script>
在上面的代码中,我们在watch
选项中执行了一个异步操作(通过axios访问api),并且限制了访问这个api的频次,还可以在服务器返回最终结果之前显示不同的提示信息。这些东西都是计算属性实现不了的。除了在Vue实例的选项中使用watch
,还可以通过vm.$watch
的方式来使用它。
计算属性的 setter 方法
计算属性默认只提供 getter 方法,必要的话我们可以提供对应的 setter 方法,操作如下:
// 此处省略其它代码
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function ( newValue ) {
var names = newValue.split( ' ' )
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
现在我们可以运行vm.fullname = ' John Doe '
来测试一下,会发现 setter 被调用了且vm.firstName
和vm.lastName
的值
被更新。