做過vue開發的都知道,有兩個很重要的概念就是計算屬性和偵聽屬性,我們在平時開發當中會經常用到它們,以至于我們在面試的時候也會經常被問到這兩個之間的差別。
可能我們經常聽到或者知道的答案就是:
- computed有緩存,watch沒有緩存
- computed不支援異步,watch支援異步
- computed需要傳回值,watch不需要傳回值
- computed的值不能在data中定義,watch需要在data中定義
聽起來沒什麼問題,但是問題就在于你可能有一些疑惑,這些不同導緻它們看起來就是不同的東西,為什麼要放在一起進行比較呢?
我們可能會經常被誤導,導緻錯誤的認為這兩個屬性可以做相同的事情,它們隻是實作一個功能的不同用法,隻是在不同場景下各有優劣。
是的,我們不應該去分析它們的差別,更準确的說我們應該問計算屬性和偵聽屬性它們的特點是什麼?而真正可以與computed作比較的其實是屬于methods。
那麼接下來我們要做的,就是看一下computed、watch和methods有什麼特性,它們的用法都有哪些,以及computed和methods在實作相同功能的時候有哪些差別。
計算屬性computed
計算屬性的屬性名不需要在data中定義,表示這是一個由其他值計算得出的值,是以需要傳回值,隻不過當計算該值所用到的響應式依賴發生變化的時候,會通知該計算屬性進行重新計算,該屬性名會在收集的時候被當做新屬性存放起來,可以直接通過this.來擷取。
①當響應式依賴變化時會重新計算
<template>
<div class="pl-20px pr-20px">
名稱:雪糕<br />單價:{{price}}<br />
數量:{{count}}個<br />總價:{{totalPrice}}
<button @click="addPrice">加1元</button>
<button @click="addCount">加1個</button>
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
price: 2,
count: 1
}
},
computed: {
totalPrice() {
return this.count * this.price
}
},
methods: {
addPrice() {
this.price++
},
addCount() {
this.count++
}
}
}
</script>
<style scoped>
.pl-20px {
padding-left: 20px;
}
.pr-20px {
padding-top: 20px;
}
</style>
我們可以看到初始化的時候totalPrice的值就已經被計算出來了,當count或price任意一個發生變化的時候,都會去重新計算totalPrice的值。
②當多次使用該值時,隻會計算一次,然後将值緩存起來,以供擷取,直到響應式依賴發生變化,才會重新計算
<template>
<div class="pl-20px pr-20px">
名稱:雪糕<br />
單價:{{price}}<br />
數量:{{count}}個<br />
總價:{{totalPrice}}<br />
總價:{{totalPrice}}<br />
總價:{{totalPrice}}<br />
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
price: 2,
count: 1,
nums: 0
}
},
computed: {
totalPrice() {
console.log(`我被計算了${++this.nums}次`)
return this.count * this.price
}
}
}
</script>
計算屬性的緩存機制,主要是用來處理:如果一個值需要大量的計算才能得到,那麼當頁面中或者其他計算屬性或者其他地方用到該值的時候,隻需做一次計算即可多處使用,而不必重複計算,來避免過多的性能開銷,如果需要實時計算,不希望值被緩存,那麼使用方法就可以達到效果。
③可以傳入不同的參數,這個時候就是當做方法來調用,不再具有緩存的效果
<template>
<div class="pl-20px pr-20px">
名稱:雪糕<br />
單價:{{price}}<br />
數量:{{count}}個<br />
總價:{{totalPrice("$")}}<br />
總價:{{totalPrice("$")}}<br />
總價:{{totalPrice("$")}}<br />
</div>
</template>
<script>
let nums = 0
export default {
name: 'testComponent',
data() {
return {
price: 2,
count: 1,
}
},
computed: {
totalPrice() {
return function(sign) {
console.log(`我被計算了${++nums}次`)
return sign + this.count * this.price
}
}
}
}
</script>
通過傳回一個函數的方式,就可以對一個計算屬性進行傳參,不過此時該計算屬性就相當于使用了一個函數,不同的是,可以利用閉包的機制,來對所有的這些函數做一些初始化的操作,請看這個例子
<template>
<div class="pl-20px pr-20px">
名稱:雪糕<br />
單價:{{price}}<br />
數量:{{count}}個<br />
總價:{{totalPrice("$")}}<br />
總價:{{totalPrice("$")}}<br />
總價:{{totalPrice("$")}}<br />
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
price: 2,
count: 3,
}
},
computed: {
totalPrice() {
let m = 0
return function(sign) {
return `優惠${m}件` + sign + (this.count - m++) * this.price
}
}
}
}
</script>
同一個計算屬性多次計算可以共享同一個閉包環境中的變量。
④可以指定getter和setter來控制行為
<template>
<div class="pl-20px pr-20px">
名稱:雪糕<br />
單價:{{price}}<br />
數量:{{count}}個<br />
總價:{{totalPrice}}<br />
<button @click="totalPrice += 10">總價+10</button>
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
price: 2,
count: 1,
nums: 0
}
},
computed: {
totalPrice: {
get() {
return this.count * this.price
},
set(val) {
this.price = val / this.count
}
}
}
}
</script>
偵聽屬性watch
被偵聽的變量需要在data中定義,表示當該屬性值發生變化的時候,需要執行某些操作,是以支援函數所允許的任何行為,不需要傳回值,可以了解為這是對一個函數進行了監聽,相當于是給函數增加了addEventListener,觸發的條件就是偵聽的屬性發生了變化,然後就會執行定義的回調函數,接收兩個參數,第一個是改變後的值,第二個是改變之前的值。可以偵聽基本類型與引用類型,甚至能偵聽引用類型裡面的某個屬性。
①當偵聽一個基本類型時
<template>
<div class="pl-20px pr-20px">
名稱:雪糕<br />
單價:{{price}}<br />
數量:{{count}}個<br />
總價:{{totalPrice}}<br />
<button @click="count += 1">加1個</button>
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
price: 2,
count: 1,
totalPrice: 0
}
},
watch: {
count(nv, ov) {
this.totalPrice = this.price * this.count
}
},
created() {
this.totalPrice = this.price * this.count
}
}
</script>
我們可以看到,當count屬性變化的時候,重新計算了totalPrice的值,這裡我們在created的時候,給totalPrice進行了指派,因為如果不初始化的話,totalPrice會取預設值0,我們把created這段代碼去掉,效果就像下面這樣
這是因為隻有當屬性變化的時候才會執行偵聽的函數,去計算totalPrice的值,那麼能不能在初始化的時候就直接執行一次偵聽的函數,而不用在created裡面重複的寫了呢?
當然可以,我們把偵聽函數改成用對象的形式,并設定immediate為true
watch: {
count: {
handler: function () {
this.totalPrice = this.price * this.count
},
immediate: true
}
}
②偵聽引用類型
<template>
<div class="pl-20px pr-20px">
<div v-for="item in goods">
名稱:{{item.name}}<br />
單價:{{item.price}}<br />
數量:{{item.count}}個<br />
<button @click="item.count += 1">加1個</button>
</div>
總價:{{totalPrice}}<br />
<button @click="addGoods">追加物品</button>
<button @click="changeGoods">改變物品</button>
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
goods: [{
name: '雪糕',
price: 2,
count: 1
}],
totalPrice: 0
}
},
watch: {
goods: {
handler: function (nv) {
this.totalPrice = nv.reduce((total, item) => {
return total += item.price * item.count
}, 0)
},
immediate: true
}
},
methods: {
addGoods() {
this.goods.push({
name: '飲料',
price: 3,
count: 2
})
},
changeGoods() {
this.goods = [{
name: '辣條',
price: 5,
count: 6
}]
}
}
}
</script>
我們定義了一個引用類型的goods,設定了三種改變方式:更改對應項的數量、追加一種物品、更改定義的物品
初始化的時候我們看到立即執行發生了作用
增加雪糕的數量
我們發現總價并沒有發生改變,這說明該屬性的變化沒有被偵聽到,是以不會執行函數,那麼如何讓屬性的變化也能被偵聽到呢,就用到了deep屬性
watch: {
goods: {
handler: function (nv) {
this.totalPrice = nv.reduce((total, item) => {
return total += item.price * item.count
}, 0)
},
immediate: true,
deep: true
}
}
這時我們看到屬性的變化也會引起偵聽函數的執行
追加一種物品
這個時候沒有deep屬性,也會偵聽到變化,進而執行函數
更改定義的物品
同樣會執行函數
③偵聽引用類型的某個屬性
<template>
<div class="pl-20px pr-20px">
<div v-for="item in goods">
名稱:{{item.name}}<br />
單價:{{item.price}}<br />
數量:{{item.count}}個<br />
<button @click="item.count += 1">加1個</button>
</div>
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
goods: [{
name: '雪糕',
price: 2,
count: 1
}]
}
},
watch: {
'goods.0.count': function(nv) {
console.log('現在的數量是:' + nv)
}
}
}
</script>
當多次更改第一項的count值時,每次的變化都能夠被偵聽到
其實偵聽屬性不但可以設定為函數和對象,也可以設定為字元串和數組
當設定為字元串時,将會執行對應的方法
<template>
<div class="pl-20px pr-20px">
<div v-for="item in goods">
名稱:{{item.name}}<br />
單價:{{item.price}}<br />
數量:{{item.count}}個<br />
</div>
<button @click="addGoods">追加物品</button>
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
goods: [{
name: '雪糕',
price: 2,
count: 1
}]
}
},
watch: {
goods: 'goodsChanged'
},
methods: {
addGoods() {
this.goods.push({
name: '飲料',
price: 3,
count: 2
})
},
goodsChanged(nv, ov) {
console.log('物品發生了改變')
console.log(nv)
}
}
}
</script>
點選追加物品按鈕,則會執行對應的方法,回調函數的兩個參數同樣是修改後與修改前的值
當設定為數組時,将會在偵聽屬性發生改變時,周遊這個數組,并把數組的每一項當做回調函數進行執行,數組的每一項同樣可以是函數、對象、字元串,注意不能再嵌套數組
<template>
<div class="pl-20px pr-20px">
<div v-for="item in goods">
名稱:{{item.name}}<br />
單價:{{item.price}}<br />
數量:{{item.count}}個<br />
</div>
<button @click="addGoods">追加物品</button>
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
goods: [{
name: '雪糕',
price: 2,
count: 1
}]
}
},
watch: {
goods: [
'change1',
function change2() {
console.log('我是change2')
},
{
handler: function change3() {
console.log('我是change3')
}
}
]
},
methods: {
addGoods() {
this.goods.push({
name: '飲料',
price: 3,
count: 2
})
},
change1(nv, ov) {
console.log('我是change1')
}
}
}
</script>
點選追加物品按鈕,我們來看一下效果
方法methods
這個相信大家都已經能非常熟練的使用了,所有的方法都會預設挂載到目前執行個體上面,也就是說可以直接通過this.進行調用,這裡隻說一下當在template裡面使用時,我們能夠怎樣進行傳參
①預設傳參
<template>
<div class="pl-20px pr-20px">
名稱:雪糕<br />
單價:{{price}}<br />
數量:{{count}}個<br />
共計:{{price * count}}
<button @click="addCount">加1個</button>
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
price: 2,
count: 1
}
},
methods: {
addCount(e) {
console.log(e)
this.count++
}
}
}
</script>
當點選加1個按鈕時,會自動執行對應方法
并且會把事件對象作為參數傳遞進去
②主動傳參
<template>
<div class="pl-20px pr-20px">
名稱:雪糕<br />
單價:{{price}}<br />
數量:{{count}}個<br />
共計:{{price * count}}
<button @click="addCount(1)">加1個</button>
<button @click="addCount(10)">加10個</button>
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
price: 2,
count: 1
}
},
methods: {
addCount(ct) {
this.count += ct
}
}
}
</script>
在調用函數的地方,直接加上括号,然後把需要的參數傳遞進去就可以了,比如點選加10個,效果依然正常呈現
當然了這種方式就沒法擷取到目前的事件對象了,這時你可能會問,那我需要這個事件對象怎麼辦呢,我該如何去處理呢?别急,我們有辦法
通過$event來控制
<template>
<div class="pl-20px pr-20px">
名稱:雪糕<br />
單價:{{price}}<br />
數量:{{count}}個<br />
共計:{{price * count}}
<button @click="addCount(1, $event)">加1個</button>
<button @click="addCount($event, 10)">加10個</button>
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
price: 2,
count: 1
}
},
methods: {
addCount(a, b) {
console.log('觀察參數')
console.log(a)
console.log(b)
}
}
}
</script>
我們先點選加1個,再點選加10個
可以看到,我們可以輕易的控制參數的傳遞
還有一種就是通過包裹一層函數來控制
<template>
<div class="pl-20px pr-20px">
名稱:雪糕<br />
單價:{{price}}<br />
數量:{{count}}個<br />
共計:{{price * count}}
<button @click="(e) => addCount(1, e)">加1個</button>
<button @click="(e) => addCount(e, 10)">加10個</button>
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
price: 2,
count: 1
}
},
methods: {
addCount(a, b) {
console.log('觀察參數')
console.log(a)
console.log(b)
}
}
}
</script>
我們先點選加1個,再點選加10個,來看下效果
我們發現同樣實作了我們想要的效果
這樣方式對于使用自定義元件時尤其有用,我們不但可以接收來自元件傳遞的參數,而且可以傳入其他我們想要傳遞的參數
③作為計算屬性來使用
<template>
<div class="pl-20px pr-20px">
名稱:雪糕<br />
單價:{{price}}<br />
數量:{{count}}個<br />
共計:{{totalPrice(price, count)}}
</div>
</template>
<script>
export default {
name: 'testComponent',
data() {
return {
price: 5,
count: 3
}
},
methods: {
totalPrice(p, c) {
return p * c
}
}
}
</script>
這個時候并不是真正的計算屬性,而是跟計算屬性差不多,可以直接執行并傳回一個值,隻不過這種方式是沒有緩存的,因為此時this.totalPrice還是指向的方法,而寫成計算屬性的時候,this.totalPrice指向的是執行之後傳回的值,當響應式依賴發生改變的時候,vue會自動執行一遍,然後把this.totalPrice指向它的傳回值。
各自特點
相信看到這裡,你已經對computed、watch、methods大概有了一些了解,并清楚了它們各自的使用方式與特性,下面我們來重新歸納一下,看看你能不能了解呢
- computed和methods無需在data中定義,watch需要
- computed有緩存,是把該變量指向每次的執行結果,當響應式依賴變化時,vue會自動再次執行,而methods是把該變量指向函數,在用到的地方都會再次執行,而且當dom更新的時候,vue也會重新執行對應的方法,是以需要注意避免死循環,watch無所謂緩存不緩存
- methods可以接收傳參,watch回調會把改變後與改變前的值傳遞過去,computed不能接收傳參,如果需要給computed進行傳參,那麼它此後的行為将于methods一緻
- computed需要傳回值,methods可以有也可以沒有,當作為計算屬性使用時,需要傳回值,watch不需要傳回值
- computed需要是同步方法,methods與watch可以執行異步方法,支援函數的所有行為
- watch還可以在執行個體對象上手動添加,例如使用this.$watch添加更複雜的監聽,watch也可以監聽一個computed
總結
我們熟練的掌握了computed、watch、methods它們的特點與使用方式,才能夠在日常開發中得心應手應對一些複雜的場景,來幫助我們解決各種問題。