Vue頁面上有數量與總價。按下加一後,數量在資料物件裡已經改變,畫面上的某個數字卻還停在原值。若你曾把 reactive() 的欄位解構出來,例如 const { count } = state,先查那個變數是不是只取得當時的普通數字。它不會因為來源物件後來更新,就自動變成新數字。
問題不是所有解構都不能用,而是被解構的是什麼、後續讀取走哪條路。本文聚焦Vue 3中reactive物件的基本型別欄位,用1變成2的小例子,比較直接解構、toRefs與computed。元件props解構另有編譯器規則,不能把這個例子的結論不分版本地套到所有寫法。
普通數字不會記住它來自哪個欄位
import { reactive } from 'vue'
const state = reactive({ count: 1 })
const { count } = state
state.count = 2
console.log(state.count) // 2
console.log(count) // 1
解構時讀到的 count 是1,之後程式更新的是代理物件上的 state.count。兩個讀取位置已分開。普通數字沒有替你保存回到state欄位的路徑,因此後面再讀count仍是1。把const改成let,只讓這個變數能重新指派,不能自動建立與state的連動。
在頁面排查時,先找模板實際使用的名稱。畫面若顯示的是解構出來的count,更新state.count就可能看不出變化;畫面若直接讀state.count,就會使用代理物件。不要只在console列印整個物件,看到物件已更新就假設模板讀的也是同一處。
最小修法是繼續讀state.count
如果元件很小,沒有必要為了少寫state就解構所有欄位。維持同一個reactive物件,事件更新它,模板也讀它,資料來源通常更容易追。以下是數量輸入的小元件,初始數量為1。
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 1 })
</script>
<template>
<button @click="state.count++">
加一
</button>
<p>數量:{{ state.count }}</p>
</template>
讀取與更新都沿state.count走,省去額外的同步變數。接上自己的商品頁時,仍要處理數量上限、最小值與送出前驗證;這段只示範畫面連動,不代表購物車、庫存或結帳系統已處理。若元件有多個來源資料,先分清哪些是可編輯狀態,哪些是推導結果。
需要解構時,用toRefs保留欄位路徑
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 1 })
const { count } = toRefs(state)
state.count = 2
console.log(count.value) // 2
count.value = 3
console.log(state.count) // 3
toRefs把當時可列舉的每個屬性轉成ref,這個ref仍連到來源物件的對應欄位。本文實際比較中,state從1變2時,普通解構值留在1,toRefs的count.value跟著變2。再把count.value設成3,state.count也變3。它不是把舊值複製到另一份需要手動維護的資料。
在JavaScript中讀取ref通常用 .value;模板頂層ref會自動解包,因此模板可以寫 {{ count }}。不要因為模板省掉.value,就在事件或工具函式中也一律省略。遇到陣列、Map或巢狀物件時,自動解包的條件又不同,應對照實際資料結構與Vue文件。
toRefs不是動態監聽未來所有新增欄位的工具。它呼叫時只轉換當時可列舉的屬性;若某欄目前不存在,卻需要先建立連到該欄的ref,可以考慮 toRef(state, 'count')。不要先對空物件toRefs,再假設後來新增的每個欄位都已經有對應ref。
總價這類結果交給computed
import { reactive, computed } from 'vue'
const state = reactive({ count: 1 })
const total = computed(
() => state.count * 10
)
state.count = 2
console.log(total.value) // 20
示例單價10只是計算演示。computed的getter讀state.count,因此數量改變時會讓推導結果更新。如果把getter改成使用先前解構的普通count,依賴就可能落在不會更新的數字上。排查computed沒變時,也要檢查getter讀的究竟是哪個來源。
不要同時存一份count和一份total,再用事件逐個更新兩者;只要有一處漏改,資料就容易不同步。能由狀態推導的值通常直接計算,真正要保存的輸入才放在可變狀態。若總價還取決於折扣、運費與伺服器價格,示例的前端計算不能代替後端訂單核價。
物件欄位與props不能一起概括
如果解構出來的欄位本來是物件,情況與普通數字不同。解構後可能仍拿到該巢狀物件的代理,所以改它的內部屬性可以維持反應性;但來源物件把整個欄位替換成另一個物件時,舊變數仍指向原物件。說明問題時應指出是內部變更還是整個替換,不說「解構一律失效」。
另外,Vue 3.5的script setup支援反應式props解構,編譯器會轉換在相同區塊中的使用方式。這是defineProps相關的元件編譯行為,與本文一般reactive物件的JavaScript解構不同。若你查的是props,先核Vue版本、script setup及變數是否傳入外部函式,不能只把所有解構都換成toRefs。
從composable回傳資料時,也要把介面約定清楚。回傳一個reactive物件,使用者隨手解構基本型別欄位可能掉連動;回傳由refs組成的普通物件,解構仍拿到refs。若工具函式已經回傳ref,通常不需要再套一次toRefs,先讀它的回傳型別與文件。
別把ref放進會複製值的流程
取得ref後,仍可能在另一個位置又取出普通值。例如把count.value先存進一個普通物件,再把那個物件交給模板,後續來源更新不會自動重寫這份快照。若某份資料是刻意在送出當下固定的請求內容,複製值有其用途;若它需要長期顯示最新數量,就應維持ref或在使用當下讀來源。
計算函式也要核對呼叫時機。傳入count.value的是當次呼叫的數字;傳入一個讀取count.value的getter,則讓函式有機會在需要時重新讀取。兩者沒有通用的優劣,應按工具函式的介面使用。不要在不接受ref的函式上硬傳ref物件,結果算出NaN後才加更多轉型。
如果外部資料更新會重新建立整個state,原本toRefs連到的是舊物件,而不是那個變數的所有未來指向。一般reactive狀態宜維持同一物件並更新欄位;若業務需要整個替換資料,先考慮用ref持有整份物件,再確認模板與工具如何讀取。不要把「有了toRefs」當作任何替換方式都自動安全。
表單需要取消編輯時,快照可以是刻意的
會員資料編輯常需要先保留原值,讓取消操作能回復。這份快照本來就不應隨每次輸入一起變動,不能看到兩份資料數字不同就一律改成toRefs。先確認哪份是可編輯狀態、哪份是原始資料,再決定保存副本或保持引用。巢狀物件的淺複製仍可能共享內部資料,取消行為需要另外檢查。
修連動問題時,保留原本的資料用途,不為了讓所有數字一起變動而破壞取消或提交時的固定內容。最小測試可以包含加一、直接輸入、取消及重新開啟表單四步;每一步列出預期的來源值與顯示值,比只看初始畫面更容易發現錯誤的引用關係。
用三個值確認你修的是同一條路
按一次按鈕後,依序讀state.count、解構變數及模板顯示值。修法採直接讀state或toRefs後,確認數量與推導結果一起改變;再測試使用者輸入與程式指定的更新是否一致。若數量正確但畫面某一行還錯,回到該行模板名稱,別立即加watch來手動補值。
watch適合需要發生副作用的工作,例如資料變更後發送請求;單純同步兩份相同數字,很可能只是掩蓋資料來源已分開的問題。若你需要更多推導欄位,可接著看 Vue computed與購物小計;若是列表刪行後輸入跑位,則屬於另一個問題,參考 列表key與輸入狀態。
示例使用Vue 3.5.43反應性runtime,結果比較為1→2→3與單價10的計算,沒有串接商品、庫存或結帳API。props行為另按元件編譯版本核對。

評論0