常見的 vue 模式,以及你什麼時候該用他們。

什麼時候你該用 computed,什麼時候該用 watch

前言

Vue 是一個可以根據你修改的資料自動更新畫面的 framework (又稱 reactive programing), 在此同時 vue 又提供了各種工具,允許你去觀測資料變化 (watch),或是轉換資料格式已呈現到 UI 上 (computed),但是有時候,你可能會不知道用什麼方式才能最簡單的達成你的目的。

比較

以 data/computed/watch/method 為例

Q: 什麼東西能直接被模板使用?
A: data/computed/method,在這三個東西上的屬性可以直接在模板中使用

Q: 什麼東西設計上不該造成副作用?
A: computed,他本身不該造成任何寫入動作

Q: 設計上你可以修改 data 的時機是什麼?
A: 觸發事件時、watch 到資料變更時、其他外部事件

如果以權責區分的話

Template 負責輸出頁面,
Data 保存用來應該顯示到縣夜市的資料
Computed 用來預先轉換 data,以使用在 template 上
Watch 觀測資料變更,並且在資料變更後觸發特定動作
Method 單純是個整裡 function 的地方,允許寫在他上面的 function 在整個 component的範圍內被使用

如果以片語的方式表達的話

我們可以把 template / watch / computed 所做的事用特定句型表達

在(A)時顯示(B) (v-if/v-else-if/v-else)

<template>
  <B v-if="A" />
</template>

範例

counter 變為 0 (A) 時顯示 時間到 (B)

<template>
  <div v-if="counter === 0">時間到</div>
</template>

顯示 (A) 經過 (B) 過程所得到的結果 (computed)

  • 副作用:無
<script>
export default {
  data: { A },
  methods: { B },
  computed: {
    result () {
      return this.B(this.A)
    }
  }
}
</script>

範例

顯示 counter (A) 取整數的 (B) 結果

<script>
export default {
  data: {
    // (A)
    counter: 1.5
  },
  methods: {
    // (B)
    floor(input) {
      return Math.floor(input)
    }
  },
  computed: {
    result () {
      return this.floor(this.counter)
    }
  }
}
</script>

顯示 表單上所有的欄位 (A) 是否都正確填寫 (B)

<script>
export default {
  data: {
    // (A)
    form: {
      A, B, C, ...
    }
  },
  methods: {
    // (B)
    checkForm(form) {
      for (let key of Object.keys(form)) {
        if (!check(form[key])) {
          return false
        }
      }

      return true
    }
  },
  computed: {
    formValid () {
      return this.checkForm(this.form)
    }
  }
}
</script>

顯示 動物清單 (A) 中 是否含有老鼠 (B)

<script>
export default {
  data: {
    // (A)
    動物清單: ['老鼠', '', '']
  },
  methods: {
    // (B)
    含有老鼠(list)) {
      return this.動物清單.contains('老鼠')
    }
  },
  computed: {
    是否含有老鼠 () {
      return this.含有老鼠(this.動物清單)
    }
  }
}
</script>

當(A)改變時進行(B) (watch)

  • 副作用:
<script>
export default {
  data: { A },
  methods: { B },
  watch: {
    A () {
      this.B()
    }
  }
}
</script>

範例

counter 變為 0 (A) 時 將用戶導到首頁 (B)

<script>
export default {
  data: {
    counter: 9
  },
  methods: {
    // (B)
    goHome() {}
  },
  watch: {
    // (A)
    counter (newVal) {
      if (newVal === 0) {
        this.goHome()
      }
    }
  }
}
</script>

複合描述

這些句形可以互相組合,變成更複雜的句型,
像是我們可以這樣組合上面的範例,
只要你能表達的句子,就能有對應的程式碼。

範例

在[[表單上所有的欄位][是否都正確填寫]]為非時,[顯示表單錯誤](v-if)
|-表單上所有的欄位是否都正確填寫(computed)
| |-表單上所有的欄位
| |-是否都正確填寫
|-顯示表單錯誤
<template>
    <div v-if="!formValid">表單錯誤</div>
</template>
<script>
export default {
  data: {
    // (A)
    form: {
      A, B, C, ...
    }
  },
  methods: {
    // (B)
    checkForm(form) {
      for (let key of Object.keys(form)) {
        if (!check(form[key])) {
          return false
        }
      }

      return true
    }
  },
  computed: {
    formValid () {
      return this.checkForm(this.form)
    }
  }
}
</script>

關於 watch 的使用時機

A: 別用

其實大多數情況下你不該用它, 除了特地造成不可逆變化的狀況(像是頁面挑轉之類), watch 都只會讓你的程式更難除錯。

在 watch 中修改 data 的話,
就等於是在資料更新後又立刻清掉舊的資料(因為你寫入了data,原本的data當然不見了),
而且你無法找到修改來源(vue devtool 沒辦法告訴你最後一次修改data的是誰),
在有超過一個以上的路徑觸發資料修改的情況下,
除非使用 debugger,
你將完全無法知道為什麼欄位被修改成你意料之外的數值。

如果 watch 中修改的 data 又觸發其他 watch 的話,
這個情況將更加嚴重,
你將會有更多的出錯路徑,更多的資料連鎖錯誤,
而你完全不知道在什麼開始時機點出錯的(畢竟舊資料被覆蓋了)。

除非你有什麼用 computed/data 無法實現的問題(例如手動追蹤 parent 來的 property 來配合第三方 library 之類),你不應該用 watch,他是一把會刺傷你的手的雙面刃,你不該在沒有理由的情況下去用它。

我該怎麼決定用什麼

怎麼決定該將計算完的資料放進 data 或是直接在 computed 內計算出資料?

你要顯示的東西可以從其他欄位直接計算嗎?
        |
    --Y---N--
    |       |
computed   data

如果你可以直接把它算出來,那你實在沒有理由去再另外存一份它

結語

Vue 跟 jQuery 等 DOM 操作工具在設計理念上有所差異,
不再是你拿著資料自己修改畫面,而是你定義了畫面如何依照規則改變後,由 Vue 幫你更新所有的畫面,
有時候,需要更多的思索才能跳出舊的思考框架,貼合 Vue 設計理念的代碼,
但是當你成功進入 vue 的思考模式後,你可以從中受益,
寫出更接近你的思考的代碼,去掉不必要的中間過程(寫 你想出現的效果 ,而不是寫 達成你想出現的效果的步驟 ),
這可以讓你寫出更簡短而且也更容易被人理解的代碼。

而這描述最終結果而不是過程的方式也就是 Declarative programming(宣告式程式設計),也是目前其他主流框架(react/angular)的設計。

會在你學習其他主流框架時給你很大的幫助。

Written on November 18, 2019