主題:即時監聽欄位變化,動態更新甜度、冰量等選項,並介紹快取策略與效能優化。
昨天我們完成了 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_MAP
const 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 |