簡短答法:computed 最大特點是必須回傳一個值,並且會把它緩存起來,當函式裏的依賴改變時,才會重新執行和求值。但 watch 與 methods 不會強制要求回傳一個值,它們只需執行動作,不一定要回傳值。watch 會偵測單一個值,當它有變化時就執行。至於 methods,只要呼叫它,它就會執行,但 computed 和 watch 則不是透過呼叫來執行。
以下會再作詳細解說。
computed 屬性的最明顯特點:
this.某computed 函式 = 123
會報錯。最後兩點很直白,以下就不作解釋。第 2 點會涉及到 computed 本身的 getter 機制,明天的文章才會針對作說明。
因此以下先解釋第一點。
看看 Vue 官方執行 computed 的解釋:
计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。
什麼是響應式依賴?意思是在一個 computed 函式裏,它所用到在 data
建立的資料。
一旦這些資料有變化,這個 computed 函式就會被重新執行和求值。
舉例就,以下 total
的依賴就是 this.price
, this.quantity
, this.discount
。只要其中一樣有變化,都會重新執行 total,並回傳新的值:
computed:{
total(){
return this.price * this.quantity * this.discount
}
}
以下情況,如果 greeting
有變化,會否觸發 total?
computed:{
total(){
console.log(this.greeting)
return this.price * this.quantity * this.discount
}
}
會。即使 greeting
並沒有用來計算 total
,但它也是在 total
這個 computed 函式內,因此也是其中一個依賴。
再看一個例子,如果 quantity 有變化,以下會否觸發 total?
let quantity = 1;
computed:{
total(){
return this.price * quantity * this.discount
}
}
不會。因為 quantity
不是在 data
裏建立,Vue 不會偵察到 quantity
的變化。所以不會觸發 total
。
關於 Vue 如何偵測到 data 資料的變化,可參考此系列之前的文章:
Vue 2:在 Vue 2 為何無法直接修改物件型別資料裏的值?
Vue 3:Vue 3 如何使用 Proxy 實現響應式(Reactivity)?
意思是當 computed 裏的所有依賴都沒發生變化,此 computed 函式就會一直回傳之前儲存起來的值。
舉例說,我按下按鈕就會把 num
變成 1。
<div id="app">
<button @click="num = 1">按我改num</button>
<p> 用add方法把以下的值由0變1:</p>
<p> {{ add }} </p>
</div>
data(){
return{
num: 0,
}
},
computed:{
add(){
console.log('我有被觸發了!')
return this.num
}
}
剛建立元件時,會印出 「我有被觸發了!」。
然後,第一次按下按鈕後, num
變成 1,會印出「我有被觸發了!」。但之後按按鈕,add
就不會再觸發,而「我有被觸發了!」這句也不會印出來。
因為每次按按鈕,都同樣是把 num
修改為 1,跟之前儲存起來的數值 1 相比並沒有變化。因此 add
不會被執行,而畫面只會一直顯示 1 這個之前已緩存起來的值。
程式碼:
https://codepen.io/alysachan/pen/LYWmOLq?editors=1111
如果用以下做法修改 computed 的值就會報錯:
computed:{
num(){
return 1
}
},
mounted() {
this.num = 10
}
報錯:
[Vue warn]: Write operation failed: computed property "num" is readonly.
因為 computed 的運作原理只有 getter (取值)沒有 setter(存值)。因此 computed property 只是唯讀。
這部分的解說內容會比較多,留待明天的文章才針對作解釋。
methods 的概念簡單很多。跟平常我們寫 JavaScript 時使用函式的概念一樣。當它被呼叫時就會執行,可帶入參數,而結果不一定要回傳一個值,只是執行一些動作也可以。
跟 computed 明顯不同,一旦 methods 被呼叫,它不管函式裏的所有依賴有沒有變化,也會照樣執行。
重用以上例子,如果使用 methods 來印出文字:
<div id="app">
<button @click="add">按我改num</button>
<p> 用add方法把以下的值由0變1:</p>
<p> {{ num }} </p>
</div>
data(){
return{
num: 0,
}
},
methods: {
add(){
console.log('我是用method,我有被觸發了!');
this.num = 1;
}
}
程式碼:
https://codepen.io/alysachan/pen/BaWxmYz?editors=1111
跟先前的 computed 示範例子的不同:
console
不會先印出一次文字。num
變成 1 後再按按鈕,console
還是會印出文字。看看以下例子:
<div id="app">
<label for="name">輸入名稱:</label>
<input type="text" v-model="name" id="name">
<p> {{ greetUserComputed }} </p>
<p> {{ greetUserMethods() }} </p>
</div>
data(){
return{
greeting: '您好!',
name: 'Alysa'
}
},
computed: {
greetUserComputed() {
return `${this.name} ${this.greeting}`
}
},
methods:{
greetUserMethods(){
return `${this.name} ${this.greeting}`
}
}
程式碼:
https://codepen.io/alysachan/pen/QWgYGWP?editors=1011
結果是,當我在 input 欄位輸入內容,不論 methods 還是 computed 都有被執行。我們知道 computed 一定會被執行,因為依賴出現變化。但為什麼 methods 都會被執行?
Vue 官方文件說明,如果在模版裏呼叫 methods 函式,而該函式裏的有使用在 data 裏建立的資料,一旦這些資料出現變化,就會執行此 methods 函式。
截圖自 Vue 官網
因此,在畫面呼叫 methods 函式的話,一旦依賴有變化,就會被重新執行。
前兩點都很直白,但對於最後一點的說法,透過例子會更易理解。
computed
無法進行非同步工作,以 AJAX 取資料為例,下面例子是分別以 computed 和 watch 打 API 取得一筆 User 資料。
結果 computed 會回傳 [object Promise]
。至於 watch,預設是綁定一個空物件,當我按按鈕去取資料時,最後就會顯示 User 的資料:
<div id="app">
<button type="button" @click="toggleBtn = true">
取得 User
</button>
<p> {{ userComputed }} </p>
<p> {{ user }} </p>
</div>
data(){
return{
user: {},
toggleBtn: false,
}
},
computed:{
async userComputed() {
try {
const res = await axios.get('https://randomuser.me/api/')
return res.data.results[0]
}catch(error){
console.log(error)
}
}
},
watch: {
async toggleBtn() {
this.user = 'loading...'
try {
const res = await axios.get('https://randomuser.me/api/')
this.user = res.data.results[0]
}catch(error){
console.log(error)
}
}
}
程式碼:
https://codepen.io/alysachan/pen/ZEywejd?editors=1011
結果:
immediate
、deep
屬性immediate
:預設 watch 的函式需要等待它所偵測的值變動時,才會執行。但如果想一載入元件就執行,可以設定 immediate: true
。deep
:當要偵測物件裏的屬性的變動時,需使用 deep: true
。示範:
<div id="app">
<label for="name">輸入名稱:</label>
<input type="text" v-model="user.name" id="name">
</div>
data(){
return{
user: {
name: 'Alysa',
job: 'web developer'
}
}
},
watch: {
user: {
handler(newVal, oldVal){
console.log(oldVal)
console.log(newVal)
},
deep: true,
// 當元件建立好,就立即執行一次此函式
immediate: true
},
// 無法偵測物件裏屬性的變動
// user(newVal, oldVal) {
// console.log(oldVal)
// console.log(newVal)
// }
// 以下是錯誤寫法
// user(newVal, oldVal) {
// deep: true,
// immediate: true,
// console.log(oldVal)
// console.log(newVal)
// }
}
程式碼:
https://codepen.io/alysachan/pen/yLXZbeB?editors=1010
結果是,當使用了 deep 和 immediate 屬性,剛載入畫面時,console 就會印出數值。之後當我在 input 欄位輸入內容,watch 也能偵測到屬性的變動,因此再次印出數值。
注意,當你輸入了內容,會發現 newVal 和 oldVal 都是一樣,因為物件是 pass by reference,所以新舊值都會是相同。
你會發現,有些情況即使使用 computed、watch 和 methods 都能實現同一效果。但是,computed
的效能通常都會比較好,因為:
第一點,官方例子有清晰說明:
截圖自 Vue 官網
當使用 watch,就要建立兩個函式。反之, 只需建立一個 computed 函式就能完成同樣功能。
第二點,因為當依賴沒變化時,computed 不會執行,直接回傳緩存的值。假設該 computed 函式裏每次執行都要跑一個極長的陣列資料,最後回傳一筆處理好的陣列。每次執行時會效能耗損都不少。對比起只要一呼叫,就一定會執行的 methods,computed 的緩存機制就比較有利。
computed | methods |
---|---|
如果computed 的響應式依賴沒有改動,就不會觸發。 |
每次觸發methods ,methods 裏的函式一定會執行 |
有緩存資料的功能 | 沒有緩存資料的功能 |
不可帶入參數 | 可帶入參數 |
可在HTML裏直接使用該computed函式所回傳的值,因為computed函式本身是有get函式,最後一定會回傳一個值 | methods本身沒有規定一定要回傳一個值 |
注意:
this.computed 函式 = 123
來修改 computed 函式的值。computed | watch |
---|---|
偵測到資料變動時,會回傳資料 | 偵測到資料變動時,不會回傳值,只會執行工作 |
能偵測多個值的變動(只要該資料是在該computed函式裏和data屬性裏) | 只能偵測一個值 |
不能帶入參數 | 能帶入參數,第一個參數是新值,第二個參數是舊值 |
Vue 的 computed、methods 跟 watch 差在哪裡?(上)
認識 Vue.js watch 監聽器