iT邦幫忙

2025 iThome 鐵人賽

DAY 8
1
Vue.js

Vue3.6 的革新:深入理解 Composition API系列 第 8

Day 8: 使用 computed 與 watch 資料監控與處理

  • 分享至 

  • xImage
  •  

前一篇說明 Vue 3 有 Proxy 管家的大絕「響應式」,但是除了響應式實戰上還會需要處理或是監控,所以第一階段的基礎理解與環境設定聊完後,今天開始我們一起進入第二階段的資料監控與處理。

首先,先來了解資料監控的 computedwatch

在 Vue 3 中,computedwatch 都能「監控資料變化」,但定位與使用場景完全不同:

  • computed → 資料衍生(算出新值),如果需要基於某些反應式資料計算出新的值,可以使用 computed 屬性。這些屬性會自動依賴相應的資料,並在其依賴改變時自動更新。
  • watch → 副作用處理(觸發行為),當需要針對某些資料變化進行特定的操作時,可以使用 watch。這讓你能夠在資料改變時執行副作用,例如傳送請求或更新其他狀態。

初學時常搞混,今天我們一起徹底釐清兩者的差異與最佳實踐。

computed(計算屬性) → 側重於「資料衍生」


computed 是一個計算屬性,主要用於依賴其他資料計算得出新的屬性。當依賴的資料改變時,computed 會自動重新計算並更新其值。

  • 定位:當某個資料(state)可以「由其他資料推導出來」時,就用 computed
  • 特點
    • 依賴響應式:自動追蹤依賴的 ref / reactive 資料。
    • 快取機制:只有在依賴改變時才會重新計算,否則直接回傳之前的結果(避免不必要的重算)。
    • 適用場景
      • 資料轉換(例如:金額格式化、過濾清單、計算總和)。
      • 畫面顯示邏輯(例如:狀態文字、CSS class)。

其實,可以把它理解成 Excel 裡的公式儲存格,只要輸入變了,公式就會重新計算。

  • 範例:這是一個間單的計算,這裡的 total 就是「監控 price、quantity,衍生出來的資料」。
<script lang="ts" setup>
import { ref, computed } from 'vue'

// 基礎資料
const price = ref<number>(100)
const quantity = ref<number>(2)

// 衍生資料:總價
const total = computed<number>(() => price.value * quantity.value)
</script>

<template>
  <div>總價:{{ total }}</div>
</template>

computed 常見坑


把副作用寫進 computed

⚠️ 問題:computed 應該只回傳新值,不要觸發副作用。

const username = ref('')
const saveName = computed(() => {
  localStorage.setItem('username', username.value) // 這是副作用

  return username.value.toUpperCase()
})

解法:用 watch 做副作用,computed 專心算值。

沒有依賴資料就會「不更新」

⚠️ 問題:computed 只有在依賴的 ref / reactive 改變時才會重新計算,如果計算裡的變數不是 reactivecomputed 不會重新跑。
解法:確認你用的是 ref / reactive,不是普通變數。

computed 當方法 (誤用場景)

⚠️ 問題:有些人把 computed 當成 function 一直呼叫,結果沒必要的時候也重算。
解法:如果是「每次呼叫都要算一次」→ 用 methods,不是 computedcomputed 適合「根據資料變化快取」,而不是「每次呼叫臨時計算」。

computed setter 忘記寫(雙向綁定失敗)

⚠️ 問題:如果用 v-model 綁定 computed,但沒定義 setter,會報錯或沒反應。
解法

<script lang="ts" setup>
import { ref, computed } from 'vue'

const firstName = ref<string>('Monica')
const lastName = ref<string>('ku')

// 雙向綁定 computed
const fullName = computed<string>({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (val: string) => {
    const parts = val.split(' ')
    firstName.value = parts[0] || ''
    lastName.value = parts[1] || ''
  }
})
</script>  

<template>
  <input v-model="fullName" placeholder="輸入全名" />
  <p>名字:{{ firstName }}</p>
  <p>姓氏:{{ lastName }}</p>
</template>

watch(監聽器) → 側重於「副作用處理」


watch 是一個跟蹤資料變化的工具,當指定的資料變化時,會執行相應的回撥函式。適用於執行非同步操作或當需要執行某些操作時,而這些操作不需要計算新值。

  • 定位:當資料變動時,需要「執行某些動作」時使用(不一定是算出新值)。
  • 特點
    • 可以精確監控 單一或多個響應式資料
    • 適合處理 非同步行為(API 請求、localStorage 更新、DOM 操作…)。
    • 無快取機制,每次變動都會觸發回呼函數。
  • 適用場景
    • 資料變了 → 打 API。
    • 資料變了 → 儲存到 localStorage。
    • 資料變了 → 控制動畫或手動更新某些 DOM。

可以理解成:監控資料變化,然後「觸發行為/副作用」。

  • 範例:表單類很常見到輸入框,通常會需要做到響應式,還記得前面 computed 提到的坑嗎?把副作用寫進 computed,因為 computed 只會回傳新值,所以不會觸發!類似的範例,如果要把資料寫入 localStorage 要改用 watch
<script lang="ts" setup>
import { ref, watch } from 'vue'

const username = ref<string>('')

// 資料變化 → 執行副作用
watch(username, (newVal: string) => {
  localStorage.setItem('username', newVal)
})
</script>

<template>
  <input v-model="username" placeholder="輸入名稱" />
</template>

watch 常見坑


watch 觸發太頻繁

⚠️ 問題:比如監聽 input,每次輸入一個字就打 API。
解法:加 debounce/ throttle,或用 watchEffect + 計時器控制。

<script setup lang="ts">
import { ref, watch, onBeforeUnmount } from 'vue'
import axios, { AxiosResponse } from 'axios'

interface SearchResult { id: number; name: string }
const results = ref<SearchResult[] | null>(null)

const query = ref<string>('')        // 使用泛型指定字串
const loading = ref<boolean>(false)

let timer: ReturnType<typeof setTimeout> | null = null
let controller: AbortController | null = null

/** 監聽輸入框 */
watch(query, (q: string) => {
  if (timer) clearTimeout(timer)     // 每次輸入重置計時器

  if (!q) {
    results.value = null
    return
  }

  timer = setTimeout(async () => {
    if (controller) controller.abort()    // 取消上一個請求
    controller = new AbortController()

    loading.value = true
    try {
      const res: AxiosResponse<any> = await axios.get(
        `/api/search`,
        {
          params: { q },
          signal: controller.signal,
        }
      )
      results.value = res.data
    } catch (err: any) {
      if (axios.isCancel(err)) {
        console.log('Request canceled', err.message)
      } else if (err.name === 'CanceledError') {
        console.log('Fetch aborted')
      } else {
        console.error(err)
      }
    } finally {
      loading.value = false
    }
  }, 500)
})

onBeforeUnmount(() => {
  if (timer) clearTimeout(timer)
  if (controller) controller.abort()
})
</script>

<template>
  <input v-model="query" placeholder="搜尋..." />
  <div v-if="loading">Loading...</div>
  <pre>{{ results }}</pre>
</template>

忘記 deep: true

⚠️ 問題:reactive 物件或陣列裡的深層屬性改了,預設 watch 不會觸發。
解法

<script lang="ts" setup>
import { reactive, watch } from 'vue'

interface User {
  name: string
  age: number
}

const user = reactive<User>({
  name: 'kuku',
  age: 20
})

// 監聽整個物件(需加 deep: true)
watch(
  () => user,
  (newVal, oldVal) => {
    console.log('User 改變:', newVal, oldVal)
  },
  { deep: true }
)
</script>

監聽 reactive 整個物件,拿不到新值

⚠️ 問題:watch(reactiveObj, (newVal) => { ... }) 其實拿到的 newValoldVal 可能一樣,所以 newVal === oldVal 會是 true,無法用物件參考比較來判斷「哪裡變了」。

<script setup lang="ts">
import { reactive, watch } from 'vue'

const state = reactive({
  count: 0,
  nested: { a: 1 }
})

// 當監聽整個 reactive 物件時,回呼的 newVal/oldVal 常為相同 Proxy
watch(state, (newVal, oldVal) => {
  console.log('new === old ?', newVal === oldVal) // 通常會印 true
  console.log('new.nested.a', newVal.nested.a, 'old.nested.a', oldVal.nested.a)
}, { deep: true })

// 觸發變更(在真實元件中通常是某個事件)
setTimeout(() => {
  state.nested.a = 2
}, 1000)
</script>

解法:監聽具體屬性。

<script setup lang="ts">
import { reactive, watch } from 'vue'

const state = reactive({ user: { name: 'kuku', age: 20 } })

// 監聽巢狀屬性 user.name => 單一監聽
watch(
  () => state.user.name,
  (newName, oldName) => {
    console.log('name changed:', oldName, '→', newName)
  }
)

state.user.name = 'rober' // 會觸發
</script>
<script setup lang="ts">
import { reactive, watch } from 'vue'

const state = reactive({ a: 1, b: 2 })

// 當要同時監聽多個欄位並一次拿到所有新舊值
watch(
  [() => state.a, () => state.b],
  ([newA, newB], [oldA, oldB]) => {
    console.log('a', oldA, 'to =>', newA)
    console.log('b', oldB, 'to =>', newB)
  }
)

state.a = 10
state.b = 20
</script>

在 setup 外部使用 watch

⚠️ 問題:watch 必須在組件 setup 週期內呼叫,否則沒有有效的響應式上下文。
解法:把 watch 寫在 <script setup> 裡。

誤用 watch 當作 computed

⚠️ 問題:很多人用 watch 來「計算新資料」,但這樣不能快取,每次都重新跑。

<script setup lang="ts">
import { ref, watch } from 'vue'

const firstName = ref('Monica')
const lastName = ref('ku')
const fullName = ref('')

// 這樣用 watch,每次都會重算,而且 fullName 只是被動塞值
watch([firstName, lastName], ([f, l]) => {
  fullName.value = `${f} ${l}`
})
</script>

<template>
  <div>{{ fullName }}</div>
</template>

解法:如果是「資料衍生」→ computed;「需要副作用」→ watch

<script setup lang="ts">
import { ref, computed } from 'vue'

const firstName = ref('Ada')
const lastName = ref('Lovelace')

// 直接「回傳一個響應式值」當變數用
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>

<template>
  <div>{{ fullName }}</div>
</template>

computed vs watch

參考資料


  1. Vue3 中 watch 的最佳實踐
  2. vue3 watch 監聽多個值(超詳細)

上一篇
Day 7: Vue 3 如何利用 Proxy 實現響應式系統
系列文
Vue3.6 的革新:深入理解 Composition API8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言