iT邦幫忙

2021 iThome 鐵人賽

DAY 20
1
Modern Web

不只懂 Vue 語法:Vue.js 觀念篇系列 第 20

不只懂 Vue 語法:試解釋 computed、watch 與 methods 的差異?

  • 分享至 

  • xImage
  •  

問題回答

簡短答法:computed 最大特點是必須回傳一個值,並且會把它緩存起來,當函式裏的依賴改變時,才會重新執行和求值。但 watch 與 methods 不會強制要求回傳一個值,它們只需執行動作,不一定要回傳值。watch 會偵測單一個值,當它有變化時就執行。至於 methods,只要呼叫它,它就會執行,但 computed 和 watch 則不是透過呼叫來執行。

以下會再作詳細解說。

computed 的特點

computed 屬性的最明顯特點:

  • 當元件被創建時(created 生命週期),computed 函式會被建立和執行一次。之後如果依賴沒有更新,就不會重新執行和求值,而是回傳之前緩存起來的值。
  • computed 的值只能被該 computed 函式修改,不能被其他方法修改,例如 this.某computed 函式 = 123 會報錯。
  • computed 的函式必須要回傳一個值。
  • computed 的函式無法傳入參數。

最後兩點很直白,以下就不作解釋。第 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 的值就會報錯:

computed:{
    num(){
        return 1
    }
},
mounted() {
    this.num = 10
}

報錯:

[Vue warn]: Write operation failed: computed property "num" is readonly.

因為 computed 的運作原理只有 getter (取值)沒有 setter(存值)。因此 computed property 只是唯讀。

這部分的解說內容會比較多,留待明天的文章才針對作解釋。

methods 的特點

  • methods 函式被呼叫就一定會執行。
  • methods 函式不一定要回傳一個值。
  • methods 函式可傳入參數。

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 還是會印出文字。

但在畫面呼叫 methods,只要資料有變化,它就會被執行?

看看以下例子:

<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 函式的話,一旦依賴有變化,就會被重新執行。

watch 的特點

  • watch 會偵測某個值,當該值有變化時,就會執行。
  • watch 可傳入參數,第一個參數是更新後的值,第二是舊值。
  • 比起 computed,可以處理非同步工作。

前兩點都很直白,但對於最後一點的說法,透過例子會更易理解。

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

結果:

immediatedeep 屬性

  • 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、watch 和 methods 都能實現同一效果。但是,computed 的效能通常都會比較好,因為:

  • 減少程式碼。
  • 當處理資料量多的資料時,因為緩存資料的機制,效能會比較好。

第一點,官方例子有清晰說明:

截圖自 Vue 官網

當使用 watch,就要建立兩個函式。反之, 只需建立一個 computed 函式就能完成同樣功能。

第二點,因為當依賴沒變化時,computed 不會執行,直接回傳緩存的值。假設該 computed 函式裏每次執行都要跑一個極長的陣列資料,最後回傳一筆處理好的陣列。每次執行時會效能耗損都不少。對比起只要一呼叫,就一定會執行的 methods,computed 的緩存機制就比較有利。

總結

computed methods
如果computed的響應式依賴沒有改動,就不會觸發。 每次觸發methodsmethods裏的函式一定會執行
有緩存資料的功能 沒有緩存資料的功能
不可帶入參數 可帶入參數
可在HTML裏直接使用該computed函式所回傳的值,因為computed函式本身是有get函式,最後一定會回傳一個值 methods本身沒有規定一定要回傳一個值

注意:

  • 無法使用 this.computed 函式 = 123 來修改 computed 函式的值。
  • 當在模版中呼叫 methods 時,只要該函式裏的依賴有變化,就會被執行。
computed watch
偵測到資料變動時,會回傳資料 偵測到資料變動時,不會回傳值,只會執行工作
能偵測多個值的變動(只要該資料是在該computed函式裏和data屬性裏) 只能偵測一個值
不能帶入參數 能帶入參數,第一個參數是新值,第二個參數是舊值

參考資料

Vue 的 computed、methods 跟 watch 差在哪裡?(上)
認識 Vue.js watch 監聽器


上一篇
不只懂 Vue 語法:請示範如何使用 Vue 3 的 teleport?
下一篇
不只懂 Vue 語法:試說明 computed 的 get 與 set 運作機制?
系列文
不只懂 Vue 語法:Vue.js 觀念篇31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言