主題:即時監聽欄位變化,動態更新甜度、冰量等選項,並介紹快取策略與效能優化。
昨天我們完成了 CRUD 與 axios 的 API 串接,資料已能正確保存到後端。
今天要聚焦表單互動體驗:當使用者選了不同飲料,我們希望其他選單能即時連動,避免選到不合法的組合。
有時候我們會希望state直接做改變
有時候你喝飲料會希望多一種新口味~
但是像巧克力只有坐熱的
系統要怎麼設計比較好呢??
我們來思考一下: 
飲料多了「巧克力」選項。
規則:只提供「熱飲」,所以當使用者從其他飲料切換到「巧克力」時:
這就是 watch 派上用場的時候。
這邊給一個小觀念先提到computed是根據計算而成的新值並不會影響原本設定的state
故事背景
當飲料菜單新增「巧克力」後,因為商品特性,只能製作「熱飲」。
我們希望當使用者點選「巧克力」時,自動將「冰量」重置為「熱飲」並鎖定,避免錯誤訂單。
選擇飲料
重置冰量
💡 核心需求:即時偵測 drink 的變化,並且自動修正 ice,這正是
watch的最佳場景。

這張圖描述了「前端偵測飲料選擇→watch 監聽→重置冰量→送出訂單」的完整流程。
watch:即時監控並觸發動作用途:當特定的 ref 或 reactive 變數值改變時,立即執行回呼函式。
對應範例
watch(drink, (newDrink) => {
  if (newDrink === '巧克力') {
    ice.value = '熱飲'   // 強制設定
    sweetness.value = '' // 也可以選擇重置甜度
  }
})
觸發時機:一旦 drink 的值改變,就自動執行。
特點:
完全自動、即時反應。
不依賴特定的使用者操作(例如按鈕或 change 事件),只要狀態改變就會觸發。
可以保證資料永遠一致:任何地方改到 drink 都能即時觸發修正。
這邊或許有人會想說? 大法師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 | 
| 副作用管理 | 適合即時修正狀態 | 適合一次性的互動邏輯 | 
| 可測試性 | 方便針對狀態改變寫測試 | 方便針對使用者事件寫測試 | 
結論與建議
當需求是「狀態改變就要做事」(例如:不論前端哪裡改了 drink 都要強制重置冰量),=> 用 watch 更安全。
當需求是「只在特定事件發生時做事」(例如:只在使用者手動切換飲料才動作),=> 用 method 也可以。
有時候因為api retuen某些值改變或是其他component影響state,那麼沒處理好可能會造成奇妙的bug
這時候我就會建議使用watch,但是把握其實用methods就好~ 畢竟watch其實可能會造成效能問題的
computed:計算屬性(複習~)在早期我們其實就有用過computed了
但是今天再把它拿出來講一下
可以查資料發現說明如下~
computed 是「被動計算」;watch 是「主動監控、可以執行副作用(例如重置欄位或呼叫 API)」。| 技術 | 適用情境 | 是否能直接改資料 | 
|---|---|---|
| computed | 顯示衍生值,如總杯數、金額 | ❌ 只能讀取 | 
| watch | 需要副作用,如重置表單、呼叫 API | ✅ 可以修改資料 | 
其實白話文說:
1.你如果會要改變其他的state建議用watch。
2.如果你需要使用總和或是根據原本定義的state去做計算且不會影響到其他state,那麼用computed比較適合
紅色這句話是什麼意思?
就是說今天假如你定義了飲料、折扣、價格的state
他如果根據你的飲料品項不同,會改變折扣跟價格(從後端拉api改變整個列表)那麼這時候就不能用compouted
但是你卻可以根據這些金額去總和出totalprice 這個新的狀態來呈現到前端
總結
watch(drink) 觸發。POST /api/orders 的格式。| 功能 | 適合使用 computed | 適合使用 watch | 
|---|---|---|
| 目的 | 推導值:根據其他狀態即時計算出新值,結果直接用在畫面渲染。 | 副作用:狀態改變時,需要執行額外動作,例如重置欄位、呼叫 API、寫入 localStorage。 | 
| 觸發時機 | 只有相依值變動時才重新計算;沒有副作用 | 每次值變動時都會執行回呼,可做任何副作用 | 
| 範例 | 計算統計表、送出按鈕是否可點 | 切換飲料時,清空不合法的甜度/冰量 | 
watch 而非 computed?computed 只能計算結果,不會去「改變」其他狀態,因此必須使用 watch 來在狀態改變的瞬間重置不合法的選擇。<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 說清楚,給你直接拷到文章裡用。
OPTION_MAPconst OPTION_MAP = {
  紅茶:   { sweetness: ['正常甜','去糖'], ice: ['正常冰','去冰','熱飲'] },
  綠茶:   { sweetness: ['正常甜','去糖'], ice: ['正常冰','去冰'] },
  巧克力: { sweetness: ['正常甜','少糖'], ice: ['熱飲'] } // ✅ 巧克力只能熱
}
目的:集中管理「飲料 → 可選甜度 / 冰量」的規則,後續加品項只改這裡。
opt(反應式)const opt = reactive({ sweetness: [], ice: [] })
目的:把「目前可選的甜度/冰量清單」存起來,提供給 <OptionGroup> 渲染。
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 = ''
})
目的:
一旦飲料改變(例如改為「巧克力」),立刻:
opt.sweetness / opt.ice
這是副作用(更動其它 state),因此用 watch 最恰當。
<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。
使用者在「步驟 1」選飲料 → drink 改變
watch(drink) 觸發:
opt.sweetness / opt.ice
sweetness / ice
「步驟 2 / 3」選單立刻改變;不合法值被清空、避免送錯
canSubmit 仍用 computed 控制送出可用/不可用
按「送出」後,送到父層(或 API)
watch 內要記得 清空不合法值,否則舊值可能殘留OptionGroup 的 :options 要綁定動態 opt.sweetness / opt.ice
drink,watch 也能正確觸發(比只綁 @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 |