iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Vue.js

需求至上的 Vue 魔法之旅系列 第 14

Day10:watch 監聽資料變化,打造更智慧的客製化選單 (watch vs computed vs method)

  • 分享至 

  • xImage
  •  

前言

主題:即時監聽欄位變化,動態更新甜度、冰量等選項,並介紹快取策略與效能優化。

昨天我們完成了 CRUD 與 axios 的 API 串接,資料已能正確保存到後端。

今天要聚焦表單互動體驗:當使用者選了不同飲料,我們希望其他選單能即時連動,避免選到不合法的組合。

有時候我們會希望state直接做改變

新情境:巧克力只能做熱的

有時候你喝飲料會希望多一種新口味~

但是像巧克力只有坐熱的

系統要怎麼設計比較好呢??

我們來思考一下: /images/emoticon/emoticon07.gif

  • 飲料多了「巧克力」選項。

  • 規則:只提供「熱飲」,所以當使用者從其他飲料切換到「巧克力」時:

    • 冰量選項必須重設,並鎖定為「熱飲」。
    • 如果先前選了「去冰」等不合法的值,也要自動清空。

這就是 watch 派上用場的時候。

這邊給一個小觀念先提到computed是根據計算而成的新值並不會影響原本設定的state


一、User Story:巧克力只能選熱飲

故事背景
當飲料菜單新增「巧克力」後,因為商品特性,只能製作「熱飲」。
我們希望當使用者點選「巧克力」時,自動將「冰量」重置為「熱飲」並鎖定,避免錯誤訂單。

1. 使用者情境

  1. 選擇飲料

    • 作為一個使用者,我想在飲料選單中選擇「巧克力」,並且系統自動幫我設定為「熱飲」,避免我誤選冰飲。
  2. 重置冰量

    • 當我切換成「巧克力」時,如果我原本選了「正常冰」或「去冰」,系統會自動清空並改為「熱飲」,避免無效組合。

💡 核心需求:即時偵測 drink 的變化,並且自動修正 ice,這正是 watch 的最佳場景。


2.時序圖

https://ithelp.ithome.com.tw/upload/images/20250928/201210527p9JSprA4h.png

這張圖描述了「前端偵測飲料選擇→watch 監聽→重置冰量→送出訂單」的完整流程。


二、 vue技術重點

1. watch:即時監控並觸發動作

  • 用途:當特定的 refreactive 變數值改變時,立即執行回呼函式。

  • 對應範例

    watch(drink, (newDrink) => {
      if (newDrink === '巧克力') {
        ice.value = '熱飲'   // 強制設定
        sweetness.value = '' // 也可以選擇重置甜度
      }
    })
    

觸發時機:一旦 drink 的值改變,就自動執行。

特點:

  • 完全自動、即時反應。

  • 不依賴特定的使用者操作(例如按鈕或 change 事件),只要狀態改變就會觸發。

  • 可以保證資料永遠一致:任何地方改到 drink 都能即時觸發修正。

補充 watch 跟 method呼叫

這邊或許有人會想說? 大法師roni

我可不可以用method來做今天的程式碼??

當然可以

比如說我們可以在optiongroup的component@change@update:modelValue 事件裡調整

<OptionGroup
  label="步驟 1:選擇飲料"
  :options="['紅茶','綠茶','巧克力']"
  v-model="drink"
  @update:modelValue="onDrinkChange"
/>

script stepup

寫我們新的methods來定義

function onDrinkChange(newDrink) {
  if (newDrink === '巧克力') {
    ice.value = '熱飲'
    sweetness.value = ''
  }
}

這樣也可以

面向 watch method(事件函式)
觸發條件 任何時候 drink 值變化 只有元件事件(例如使用者改選)觸發
資料一致性 ✅ 保證全域一致 可能遺漏非使用者操作的改變
需要在模板加事件 ❌ 不需要 ✅ 需要 @update:modelValue
副作用管理 適合即時修正狀態 適合一次性的互動邏輯
可測試性 方便針對狀態改變寫測試 方便針對使用者事件寫測試

結論與建議

  1. 當需求是「狀態改變就要做事」(例如:不論前端哪裡改了 drink 都要強制重置冰量),=> 用 watch 更安全。

  2. 當需求是「只在特定事件發生時做事」(例如:只在使用者手動切換飲料才動作),=> 用 method 也可以。

有時候因為api retuen某些值改變或是其他component影響state,那麼沒處理好可能會造成奇妙的bug

這時候我就會建議使用watch,但是把握其實用methods就好~ 畢竟watch其實可能會造成效能問題的

2. computed:計算屬性(複習~)

在早期我們其實就有用過computed了

但是今天再把它拿出來講一下

可以查資料發現說明如下~

  • 用途:根據現有的 reactive 資料,自動產生衍生值。
  • 差異說明computed 是「被動計算」;watch 是「主動監控、可以執行副作用(例如重置欄位或呼叫 API)」。
技術 適用情境 是否能直接改資料
computed 顯示衍生值,如總杯數、金額 ❌ 只能讀取
watch 需要副作用,如重置表單、呼叫 API ✅ 可以修改資料

其實白話文說:

1.你如果會要改變其他的state建議用watch。
2.如果你需要使用總和或是根據原本定義的state去做計算且不會影響到其他state,那麼用computed比較適合

紅色這句話是什麼意思?

就是說今天假如你定義了飲料、折扣、價格的state

他如果根據你的飲料品項不同,會改變折扣跟價格(從後端拉api改變整個列表)那麼這時候就不能用compouted

但是你卻可以根據這些金額去總和出totalprice 這個新的狀態來呈現到前端


總結

  • 飲料選巧克力watch(drink) 觸發。
  • 自動設定冰量為熱飲 → 保證資料一致性。
  • 後端流程不需要修改 → 因為邏輯完全在前端完成,送出的資料依然符合 POST /api/orders 的格式。

  • 何時用 watch:需要在資料改變時執行邏輯或副作用。
  • 何時用 computed:僅用於顯示與計算的衍生值。

3. watch vs computed:什麼時候用哪一個?

功能 適合使用 computed 適合使用 watch
目的 推導值:根據其他狀態即時計算出新值,結果直接用在畫面渲染。 副作用:狀態改變時,需要執行額外動作,例如重置欄位呼叫 API寫入 localStorage
觸發時機 只有相依值變動時才重新計算;沒有副作用 每次值變動時都會執行回呼,可做任何副作用
範例 計算統計表、送出按鈕是否可點 切換飲料時,清空不合法的甜度/冰量

4. 為什麼「巧克力只能做熱的」要用 watch 而非 computed

  • 這個需求不只是算出一個值,而是要主動修改其他欄位的狀態
  • computed 只能計算結果,不會去「改變」其他狀態,因此必須使用 watch 來在狀態改變的瞬間重置不合法的選擇

範例:OrderForm.vue (今天只需修改這個component)

<script setup>
import { ref, reactive, watch, computed } from 'vue' // 👉 Day10: 新增 watch
import OptionGroup from './OptionGroup.vue'

const emit = defineEmits(['submit'])

/* 表單欄位 */
const name = ref('')
const note = ref('')
const drink = ref('')
const sweetness = ref('')
const ice = ref('')

/* ---------------- Day10 改動開始 ---------------- */
// 每種飲料對應的甜度與冰量可選項
const OPTION_MAP = {
  紅茶:   { sweetness: ['正常甜','去糖'], ice: ['正常冰','去冰','熱飲'] },
  綠茶:   { sweetness: ['正常甜','去糖'], ice: ['正常冰','去冰'] },
  巧克力: { sweetness: ['正常甜','少糖'], ice: ['熱飲'] } // ✅ 巧克力只能熱
}

// 目前動態顯示的選項
const opt = reactive({
  sweetness: [],
  ice: []
})

// 監聽 drink:自動更新甜度與冰量的可選項,並清空不合法的值
watch(drink, (d) => {
  if (!d) return
  opt.sweetness = OPTION_MAP[d].sweetness
  opt.ice = OPTION_MAP[d].ice
  if (!opt.sweetness.includes(sweetness.value)) sweetness.value = ''
  if (!opt.ice.includes(ice.value)) ice.value = ''
})
/* ---------------- Day10 改動結束 ---------------- */

/* 驗證條件 */
const hasDrink = computed(() => !!drink.value)
const hasSweetness = computed(() => !!sweetness.value)
const hasIce = computed(() => !!ice.value)
const canSubmit = computed(() =>
  !!(name.value && hasDrink.value && hasSweetness.value && hasIce.value)
)

/* 送出事件 */
function addOrder() {
  if (!canSubmit.value) return
  emit('submit', {
    name: name.value,
    note: note.value,
    drink: drink.value,
    sweetness: sweetness.value,
    ice: ice.value
  })
  name.value = note.value = ''
  drink.value = sweetness.value = ice.value = ''
}
</script>

<template>
  <!-- Day 3:姓名/備註 -->
  <div :class="['block', name ? 'complete' : 'invalid']">
    <label>姓名(必填)
      <input type="text" v-model.trim="name" placeholder="請輸入你的名字" />
    </label>
    <p class="hint" v-if="!name">尚未填寫姓名</p>
  </div>

  <div class="block">
    <label>備註(選填)
      <textarea v-model.trim="note" placeholder="例如:三點拿、少冰"></textarea>
    </label>
  </div>

  <!-- Day10: 飲料可選項加入巧克力 -->
<OptionGroup
  label="步驟 1:選擇飲料"
  :options="Object.keys(OPTION_MAP)"
  v-model="drink"
  required
/>

<!-- 甜度:依飲料動態變化 -->
<OptionGroup
  v-if="drink"
  label="步驟 2:選擇甜度"
  :options="opt.sweetness"
  v-model="sweetness"
  required
/>

<!-- 冰量:依飲料動態變化 -->
<OptionGroup
  v-if="drink && sweetness"
  label="步驟 3:選擇冰量"
  :options="opt.ice"
  v-model="ice"
  required
/>

  <!-- 送出 -->
  <button
    :disabled="!canSubmit"
    @click="addOrder"
    :class="['submit', canSubmit ? 'enabled' : 'disabled']"
  >
    {{ canSubmit ? '送出' : '請完成所有必填' }}
  </button>
</template>


解釋一下程式碼片段

下面把 Day10 的改動watch 用途、與為什麼不用 computed 說清楚,給你直接拷到文章裡用。


1) 新增規則表 OPTION_MAP

const OPTION_MAP = {
  紅茶:   { sweetness: ['正常甜','去糖'], ice: ['正常冰','去冰','熱飲'] },
  綠茶:   { sweetness: ['正常甜','去糖'], ice: ['正常冰','去冰'] },
  巧克力: { sweetness: ['正常甜','少糖'], ice: ['熱飲'] } // ✅ 巧克力只能熱
}

目的:集中管理「飲料 → 可選甜度 / 冰量」的規則,後續加品項只改這裡。


2) 動態選項容器 opt(反應式)

const opt = reactive({ sweetness: [], ice: [] })

目的:把「目前可選的甜度/冰量清單」存起來,提供給 <OptionGroup> 渲染。


3) 使用 watch(drink) 監聽 & 重置

watch(drink, (d) => {
  if (!d) return
  opt.sweetness = OPTION_MAP[d].sweetness
  opt.ice = OPTION_MAP[d].ice
  if (!opt.sweetness.includes(sweetness.value)) sweetness.value = ''
  if (!opt.ice.includes(ice.value)) ice.value = ''
})

目的

  • 一旦飲料改變(例如改為「巧克力」),立刻

    1. 更新可選清單 opt.sweetness / opt.ice
    2. 若現值不合法(例:原本選了「正常冰」),自動清空,避免送出無效組合
  • 這是副作用(更動其它 state),因此用 watch 最恰當。


4) <OptionGroup> 改為吃「動態 options」

<!-- 甜度:依飲料動態變化 -->
<OptionGroup
  v-if="drink"
  label="步驟 2:選擇甜度"
  :options="opt.sweetness"
  v-model="sweetness"
  required
/>

<!-- 冰量:依飲料動態變化 -->
<OptionGroup
  v-if="drink && sweetness"
  label="步驟 3:選擇冰量"
  :options="opt.ice"
  v-model="ice"
  required
/>

目的:選單可以跟著 drink 即時切換(選「巧克力」時只顯示「熱飲」)。

為什麼不用 computed

比較點 computed watch
用途 產生衍生值(只讀) 偵測變動並執行副作用(可寫)
是否會主動改其他 state ❌ 不會 ✅ 會(本案例需重置 sweetness/ice
適用本案例? 不適合:無法重置不合法的值 適合:飲料變化就主動修正其它欄位

本案例目的是「選到巧克力就強制冰量=熱飲,且清除不合法選擇」,需要寫入別的 state,所以要用 watch,而非 computed


資料流(Step by Step)

  1. 使用者在「步驟 1」選飲料 → drink 改變

  2. watch(drink) 觸發:

    • 更新 opt.sweetness / opt.ice
    • 自動清掉不合法的 sweetness / ice
  3. 「步驟 2 / 3」選單立刻改變;不合法值被清空、避免送錯

  4. canSubmit 仍用 computed 控制送出可用/不可用

  5. 按「送出」後,送到父層(或 API)


易錯點 & 小提醒

  • watch 內要記得 清空不合法值,否則舊值可能殘留
  • OptionGroup:options 要綁定動態 opt.sweetness / opt.ice
  • ✅ 若未來從 API 回填 drinkwatch 也能正確觸發(比只綁 @change 更保險)

小結

觀念 說明
watch 監聽資料變化 → 執行副作用(重置欄位、打 API、快取)。
computed 根據現有狀態推導出新值(統計數量、顯示用文字)。
應用 「巧克力只能做熱的」:需 watch → 自動清空不合法的甜度/冰量。
優化 加入 localStorage 快取、Debounce 重算,讓表單互動順暢。

透過這個 巧克力案例,我們完整理解了 watch vs computed 的最佳實務:
computed 負責計算watch 負責行動,表單就能即時反應複雜規則,同時保有良好的效能與維護性。

其實功能還是透過我們需求去延伸的

並不適平白無故說好用就覺得我一定要用watch,現在就要用computed

這邊幫大家整理一個表格(感謝chatgpt大神的幫忙)

面向 computed watch methods(事件函式)
核心用途 由既有狀態推導出衍生值(唯讀快取) 監聽狀態變化並執行副作用(可改其他狀態、可呼叫 API) 回應使用者/事件的邏輯(點擊、輸入、提交…)
觸發時機 依賴的 reactive 值改變時 懶計算 + 快取 監聽目標值改變時 立即/深度/節流可自訂 被呼叫時(例如 @click@change、程式直接呼叫)
是否修改其他狀態 ❌ 不應該 ✅ 常見(重置欄位、同步其他值、打 API) ✅ 視需求(表單送出、切換 UI)
適用情境 顯示計算結果、格式化資料、統計彙總 需要「狀態一改就做事」的副作用:自動修正、同步、快取預載 使用者驅動的流程:提交、切換、驗證、導頁
代表範例 計算總杯數、金額、依條件產生 class 飲料改為「巧克力」→ 自動將冰量設「熱飲」、清理不合法選項 @click="addOrder()" 新增訂單、@change="onDrinkChange()"
效能特性 具備快取:依賴不變就不重算 沒快取、每次變化都執行 callback 無快取、由事件驅動
API 行為 ❌ 不建議 ✅ 常見(e.g. 值變更就打 API) ✅ 常見(e.g. 按鈕送出打 API)
常見陷阱 在 computed 內做副作用(反模式) 忘了處理初始值 / 深層物件需 deep:true 只綁 UI 事件,忽略程式內部變更會漏觸發
精簡判斷 「要顯示的結果」→ 用 computed 「值一改就要做事」→ 用 watch 使用者事件要做事」→ 用 methods

上一篇
Day 9.5 — 讓通訊更好用的魔法:用 Axios 封裝飲料訂單 CRUD
系列文
需求至上的 Vue 魔法之旅14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言