本文譯自Vue.js官方文檔
【約定:下文中的 “計(jì)算屬性” 即為 “computed屬性” 】
背景簡介
在模板內(nèi)使用表達(dá)式是非常方便的罕扎,但也只限于簡單的表達(dá)式而已,如果將過多的業(yè)務(wù)邏輯放在模板內(nèi)處理批幌,就會顯得臃腫而難以維護(hù)病曾。請看下列代碼:
<div id="example">
{{ message.split( ' ' ).reverse().join( ' ' ) }}
</div>
由此可見這個模板不再那么簡潔和表述清晰了,你需要花費(fèi)好些時間才能看出它最終要顯示什么樣的結(jié)果嫩海。當(dāng)你在模板中多次使用這樣處理過的信息時結(jié)果將變得更糟捂刺。
這也就是為什么在碰到復(fù)雜邏輯運(yùn)算時我們應(yīng)該使用計(jì)算屬性的原因谣拣。
基本用法示例
<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實(shí)例
return this.message.split( ' ' ).reverse().join( ' ' )
}
}
})
輸出結(jié)果:
Original message: "Hello"
Computed reversed message: "olleh"
上面的代碼中我們聲明了一個計(jì)算屬性reversedMessage
,這里其實(shí)只是聲明了這個屬性的一個 getter 方法族展,關(guān)于它的 setter 方法我們在本文的后面有討論森缠。
console.log( vm.reversedMessage ) // => 'olleH'
vm.message = 'Goodbye'
console.log( vm.reversedMessage ) // => 'eybdooG'
我們可以在控制臺試著改變一下上例中的vm.message
的值,你會發(fā)現(xiàn)輸出的vm.reversedMessage
的值也會立刻跟著改變仪缸。
我們可以在模板中使用計(jì)算屬性來作數(shù)據(jù)綁定贵涵,跟使用一般的屬性一個樣,Vue知道這個計(jì)算屬性vm.reversedMessage
依賴于數(shù)據(jù)屬性vm.message
,所以當(dāng)vm.message
的值變化時宾茂,vm.reversedMessage
的返回值會重新計(jì)算(也就是它的 *getter 方法會被調(diào)用)瓷马,Vue將會更新所有的引用了vm.reversedMessage
的數(shù)據(jù)。由此可見我們通過聲明了一個計(jì)算屬性的 getter 方法跨晴,使這個計(jì)算屬性與 getter 方法內(nèi)使用到的其它屬性產(chǎn)生了依賴關(guān)系欧聘,且這個方法不會產(chǎn)生副效應(yīng),使得我們更容易去測試和理解這個計(jì)算屬性的工作過程端盆。
計(jì)算屬性的緩存 vs 方法
我們可能會想到通過在模板中調(diào)用方法也能達(dá)到相同的效果:
<p>Reversed message: "{{ reverseMessage() }}"</p>
// 此處省略其它代碼
methods: {
reverseMessage: function() {
return this.message.split( ' ' ).reverse().join( ' ' )
}
}
我們在methods內(nèi)定義了一個同樣的方法來替代計(jì)算屬性怀骤。其實(shí)使用這兩種方式所達(dá)到的最終結(jié)果完全一樣,唯一的區(qū)別是計(jì)算屬性可以基于它們的響應(yīng)式依賴(代表它所依賴的某些屬性或數(shù)據(jù)是響應(yīng)式的)對計(jì)算結(jié)果作緩存焕妙,只有它的響應(yīng)式依賴項(xiàng)message
的值發(fā)生改變時蒋伦,這個計(jì)算屬性才會重新計(jì)算,也就是說只要message
的值沒有發(fā)生改變焚鹊,所有引用reversedMessage
計(jì)算屬性的地方將立刻返回先前的計(jì)算值凉敲,而不會再去調(diào)用那個 getter 方法重新計(jì)算值。這意味著下列代碼中的計(jì)算屬性now
的值永遠(yuǎn)都不會更新寺旺,因?yàn)?code>Date.now()不是響應(yīng)式的:
computed: {
now: function() {
return Date.now()
}
}
相比而言,通過在模板內(nèi)調(diào)用方法就不會緩存势决,只要頁面重新渲染阻塑, getter 方法就會被執(zhí)行。
我們?yōu)槭裁葱枰彺婺兀?/em>假設(shè)我們有一個開銷巨大的計(jì)算屬性A果复,A需要遍歷一個巨大的數(shù)組而且還要做大量計(jì)算陈莽,然后我們還有另外的一些計(jì)算屬性且它們依賴于A。如果沒有緩存的話虽抄,不管A的值有沒有發(fā)生改變走搁,A的值都要計(jì)算多次(A的 getter 被執(zhí)行多次);如果有緩存的話迈窟,在A沒有發(fā)生改變的情況下私植,那些依賴于A的計(jì)算屬性可以使用同一個A的緩存值。
計(jì)算屬性 vs watch
Vue明確的提供了一個通用的手段來發(fā)現(xiàn)數(shù)據(jù)的變化并為之做出響應(yīng)车酣,這就是使用watch
選項(xiàng)曲稼。當(dāng)一些數(shù)據(jù)需要根據(jù)其它數(shù)據(jù)的變化做出相應(yīng)的改變時,使用watch
確實(shí)是一種誘人的辦法湖员,尤其對于那些很熟悉AngularJS的同學(xué)更是如此贫悄。不過在這里我們還是更提倡使用計(jì)算屬性而不是watch
,看看下面的例子就能區(qū)分它們的優(yōu)劣了:
<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
}
}
})
上面的代碼顯得很繁瑣娘摔,但又是不可避免的窄坦。再看看下列使用計(jì)算屬性實(shí)現(xiàn)的代碼:
var vm = new Vue( {
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
} )
顯而易見,使用計(jì)算屬性的代碼簡潔多了!
通過以上的了解鸭津,我們知道在大多數(shù)情況下使用計(jì)算屬性是更合適的彤侍,然而有個時候我們確實(shí)需要自定義watcher來滿足某些需求,這也是Vue提供了一個watch
選項(xiàng)的原因曙博。假設(shè)有這樣一個需求:我們要對數(shù)據(jù)的改變做出異步響應(yīng)或做些消耗巨大的操作拥刻。請看下列代碼:
<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
選項(xiàng)中執(zhí)行了一個異步操作(通過axios訪問api)父泳,并且限制了訪問這個api的頻次般哼,還可以在服務(wù)器返回最終結(jié)果之前顯示不同的提示信息。這些東西都是計(jì)算屬性實(shí)現(xiàn)不了的惠窄。除了在Vue實(shí)例的選項(xiàng)中使用watch
蒸眠,還可以通過vm.$watch
的方式來使用它。
計(jì)算屬性的 setter 方法
計(jì)算屬性默認(rèn)只提供 getter 方法杆融,必要的話我們可以提供對應(yīng)的 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]
}
}
}
現(xiàn)在我們可以運(yùn)行vm.fullname = ' John Doe '
來測試一下,會發(fā)現(xiàn) setter 被調(diào)用了且vm.firstName
和vm.lastName
的值
被更新脾歇。