Vue子元件改props連父頁也變:用emit交回修改

把會員物件傳進編輯卡片後,子元件改了 user.name,父頁的名稱竟然也跟著變。你可能以為props是唯讀,所以子元件裡的修改應該沒有作用;問題是,物件的巢狀欄位與整個prop重新賦值,是不同層的操作。

Vue的props採由父到子的資料流,但傳入物件或陣列時,子元件仍可能經由共用參照修改裡面的內容。較清楚的做法是讓子元件回報「想把名稱改成什麼」,再由父元件決定如何修改來源。這樣畫面是否變動,與誰有責任保存資料,就能分開看。

唯讀prop不等於整棵物件都不能改

以下父頁將 user 傳入子元件。子元件收到的物件仍關聯到父頁持有的來源;直接寫裡面的name,可能讓父頁一起顯示新名稱。

<!-- 子元件的問題寫法 -->
<script setup>
const props = defineProps({ user: Object })
function rename() {
  props.user.name = 'Direct'
}
</script>
<template>
  <button @click="rename">改名稱</button>
</template>

這與 props.user = anotherObject 不同。後者重新賦值prop本身,不符合單向資料流;前者改共用物件的巢狀欄位,官方文件說明Vue無法合理地阻止所有這類變動。沒有看見唯讀警告,不代表這個元件的資料修改責任就設計好了。

畫面能同步,是因為資料參照與響應式追蹤,不表示父頁已同意保存。若父頁還有「取消」與「儲存」按鈕,子元件一開始就改來源,取消時可能已經找不回原值。這種錯誤也容易藏在幾個共用同一物件的卡片之間:一處編輯,另一處展示突然提前改變。

子元件回報意圖,父元件處理來源

讓子元件發出名稱,而不是直接改props。事件名稱與參數要能表達這個操作,父元件才能驗證或拒絕它。

<!-- RenameButton.vue -->
<script setup>
defineProps({ user: Object })
const emit = defineEmits(['rename'])
function requestRename() {
  emit('rename', 'Intent')
}
</script>
<template>
  <p>{{ user.name }}</p>
  <button @click="requestRename">提出修改</button>
</template>

父頁接到事件,才更新自己的狀態。下面使用 ref持有物件,範例以新物件替換來源,保留其他欄位。這不要求所有父元件都一定替換物件;重點是修改動作發生在來源的擁有者那裡。

<script setup>
import { ref } from 'vue'
import RenameButton from './RenameButton.vue'
const user = ref({ id: 1, name: 'Ada' })
function rename(name) {
  user.value = { ...user.value, name }
}
</script>
<template>
  <p>{{ user.name }}</p>
  <RenameButton :user="user" @rename="rename" />
</template>

範例固定發出字串,真實輸入仍要在適當位置核對型別、長度與業務條件。emit只是讓父元件收到事件,不會替你做權限或伺服器驗證。若父頁要等API成功才改來源,也應在父層安排等待與錯誤顯示,不要子元件已先改、失敗時再猜怎麼還原。

用兩個按鈕看清修改路徑

本文的真瀏覽器案例使用Vue3.5.43,父頁和子卡片起始都顯示Ada。第一個按鈕直接修改巢狀name後,兩處都顯示Direct,但事件紀錄為零。這說明父來源可以在沒有收到事件的情況下被子元件改動。

重設到Ada後,第二個按鈕發出rename事件,父元件的處理函式收到Intent,紀錄舊值Ada,再更新來源。此時父頁與子卡片都顯示Intent,且有一筆事件。兩條路最後都能改畫面,但只有後一條讓修改意圖先交給父元件處理。

這個案例只操作本機資料與DOM,不含保存API。讀者在測試專案可照做相同比較:先把父頁與子元件名稱都顯示出來,點直接修改按鈕,再重設後點事件按鈕。比對的不是哪個按鈕比較快,而是父頁有沒有在自己的處理函式中接到明確的修改參數。

如果已知父子元件緊密合作,且你刻意允許子元件修改共用物件,技術上不必為了本文強行改寫所有功能。但應把這個約定寫清楚,並想好取消、重試、保存失敗時的處理。一般可重用的編輯元件採事件回報,通常更容易讓不同父頁接上各自的規則。

編輯草稿不要只複製最外層

有「確認後才套用」的編輯表單,可以讓子元件先持有本地草稿,提交時emit必要的欄位,取消時丟棄草稿。若資料有巢狀物件,單純 { ...props.user }只是淺拷貝,內部物件仍可能共用參照;在本地草稿改深層欄位,又可能動到來源。

這不表示所有表單都必須深拷貝整筆資料。對只有名稱的編輯器,直接建立一個本地字串草稿即可。需要多個欄位時,按你真正可編輯的資料建立草稿,比把整個會員物件連權限與內部欄位都複製進編輯器更容易掌握。

const draftName = ref(props.user.name)
function submitName() {
  emit('rename', draftName.value)
}

這份草稿只在建立時取來源名稱。若父頁後來切換會員,你還要決定要不要重新載入草稿、是否保留尚未提交的編輯。不要以為ref初始化一次後就會自動跟所有新props同步;需要同步時明確監聽相應來源,並避免覆蓋使用者正在編輯的值。

事件不會自動一路傳到所有祖先

Vue元件事件不像DOM事件那樣自動冒泡。父元件若只是包裝另一個子元件,需要依設計監聽並重新發出事件,或在合適層接收;不能假定最內層emit後,祖父頁一定收到。找不到處理函式時,先查模板中實際綁定的事件名稱與元件關係。

如果目標是可重用的單一輸入控制,也可以採元件v-model的更新協定。這仍是讓父來源接到更新意圖,不代表任意prop的巢狀欄位都適合直接改。選emit命名事件還是v-model,要看這個元件提供的是一個值,或是一項明確的業務操作。

例如「提出名稱修改」可以交由父頁等待保存;一般文字輸入則可能希望每次輸入都更新父持有的值。兩者的互動節奏不同。先決定使用者何時算完成編輯,再選事件設計,不要只因為某個寫法短就混用。

查修時先找來源與保存時點

在元件樹裡找出哪一層真正持有user,再搜尋子元件對props巢狀欄位的賦值。若它們提前改了來源,把修改改成事件,再讓擁有來源的父元件處理。接著測試取消、重設與切換資料,不要只測點一次按鈕能否看到新字。

保存API失敗時,畫面應保留可理解的狀態,讓使用者知道哪些只是草稿、哪些已成功。本文的emit流程可以讓保存責任集中在父頁,但仍需要你按真正功能安排錯誤處理。父子畫面同步,只證明它們讀到新的本地狀態,不能當成後端保存成功。

提交事件也應只帶父頁真正需要的資料,別把整個props物件原封不動交回去,再讓父頁覆蓋所有欄位。本例只送名稱,所以父頁能保留ID等既有資料;若有多筆會員,另帶穩定ID並核對目前編輯對象,避免事件處理了錯誤的一筆。

參考資料

原文鏈接:https://wntheme.com/vue-props-nested-mutation-emit/,轉載請註明出處。
0

評論0

顯示驗證碼
沒有帳號?註冊  忘記密碼?