本文 AI 產出,尚未審核

Vue3 元件通信:Props & Emit 完全指南


簡介

在 Vue3 專案中,元件(Component) 是打造可重用 UI 的基礎單位。無論是簡單的表單、複雜的儀表板,還是跨頁面的共享狀態,最常見的資訊流都是 父子元件之間的傳遞
propsemit 正是 Vue 官方推薦的 單向資料流 解法:父層把資料以 props 傳給子層,子層若需要回報事件或變更,則透過 emit 向父層發射事件。這樣的設計不僅讓資料流向清晰、易於追蹤,也能避免不必要的耦合,提升維護性與測試性。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,最後帶出實務應用場景,完整呈現 props / emit 在 Vue3 中的使用方式,協助初學者快速上手,也讓中階開發者能在大型專案中穩健運用。


核心概念

1. Props:父層向子層單向傳遞資料

  • 宣告:子元件必須在 props 選項中明確列出可接受的屬性,並可設定型別、預設值與驗證。
  • 單向:父層傳入的資料是只讀的,子層不應直接修改 props,若需要本地化的變更,應先在子層建立本地狀態(ref / reactive)再操作。
// ChildComponent.vue
<script setup>
import { defineProps } from 'vue'

const props = defineProps({
  // 必填的字串型別
  title: {
    type: String,
    required: true
  },
  // 帶預設值的數字型別
  count: {
    type: Number,
    default: 0
  }
})
</script>

<template>
  <h3>{{ title }}</h3>
  <p>目前計數:{{ count }}</p>
</template>
// ParentComponent.vue
<script setup>
import ChildComponent from './ChildComponent.vue'
</script>

<template>
  <ChildComponent title="計數器" :count="parentCount" />
</template>

小技巧:使用 :count="parentCount" 時,: 代表 綁定,確保傳遞的是數值而非字串。


2. Emit:子層向父層回報事件

  • 發射事件:子元件使用 emit 方法向父層傳遞訊息,父層在使用子元件時以 @事件名 監聽。
  • 事件命名:建議使用 kebab-case(例如 update-count)或 camelCase(在模板中自動轉換)保持一致性。
// ChildComponent.vue
<script setup>
import { defineEmits, ref } from 'vue'

const emit = defineEmits(['update-count'])
const localCount = ref(0)

function increase() {
  localCount.value++
  // 向父層發射事件,攜帶最新的計數值
  emit('update-count', localCount.value)
}
</script>

<template>
  <button @click="increase">+1</button>
</template>
// ParentComponent.vue
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const parentCount = ref(0)

function handleUpdate(newCount) {
  parentCount.value = newCount
}
</script>

<template>
  <p>父層顯示的計數:{{ parentCount }}</p>
  <ChildComponent @update-count="handleUpdate" />
</template>

注意emit 的第一個參數是事件名稱,後面的參數會依序傳給監聽器的回呼函式。


3. Props 與 Emit 的配合:v-model 的底層原理

Vue3 內建的 v-model 其實是 props + emit 的語法糖。預設情況下,v-model 會把父層的資料綁定到子層的 modelValue prop,並在子層 emit('update:modelValue', newValue) 時同步回父層。

// MyInput.vue
<script setup>
import { defineProps, defineEmits, ref, watch } from 'vue'

const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  }
})
const emit = defineEmits(['update:modelValue'])
const input = ref(props.modelValue)

watch(() => props.modelValue, (val) => {
  input.value = val
})

function onInput(e) {
  emit('update:modelValue', e.target.value)
}
</script>

<template>
  <input :value="input" @input="onInput" />
</template>
// Parent.vue
<template>
  <MyInput v-model="username" />
  <p>輸入的名稱:{{ username }}</p>
</template>

<script setup>
import { ref } from 'vue'
import MyInput from './MyInput.vue'

const username = ref('')
</script>

重點:若要自訂 v-model 的 prop 與事件名稱,只需要在子元件 defineProps 中使用 modelValue 以外的名稱,並在 defineEmits 中使用 update:自訂名稱,父層則使用 v-model:自訂名稱


4. 多層傳遞:從祖父元件到孫子元件

雖然 props 可以層層傳遞,但過深的嵌套會使程式碼難以維護。此時可以考慮 provide / injectpinia 等全域狀態管理,但在大多數情況下,只在需要時才使用深層傳遞,保持單向資料流的原則。

// GrandParent.vue
<template>
  <Parent :msg="greeting" @reply="handleReply" />
</template>

<script setup>
import { ref } from 'vue'
import Parent from './Parent.vue'

const greeting = ref('哈囉!')
function handleReply(payload) {
  console.log('GrandParent 收到回覆', payload)
}
</script>
// Parent.vue
<template>
  <Child :msg="msg" @reply="forwardReply" />
</template>

<script setup>
import { defineProps, defineEmits } from 'vue'
import Child from './Child.vue'

const props = defineProps(['msg'])
const emit = defineEmits(['reply'])

function forwardReply(payload) {
  // 直接把子層事件往上層傳遞
  emit('reply', payload)
}
</script>
// Child.vue
<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps(['msg'])
const emit = defineEmits(['reply'])

function sendReply() {
  emit('reply', { text: '收到!', from: 'Child' })
}
</script>

<template>
  <p>父層傳來的訊息:{{ msg }}</p>
  <button @click="sendReply">回覆</button>
</template>

常見陷阱與最佳實踐

陷阱 可能的問題 解決方案 / 最佳實踐
直接修改 Props 會觸發 Vue 警告,且破壞單向資料流 在子元件內使用 refcomputed 產生本地副本,再操作副本
事件名稱不一致 父層監聽不到子層發射的事件 統一使用 kebab-caseupdate-count)或在 TypeScript 中使用 as const 定義事件字串
過度傳遞多層 Props 元件樹過深,維護成本提升 考慮使用 provide/injectPiniaVuex,僅在必要時傳遞
忘記在父層使用 .syncv-model 子元件發射的更新無法同步回父層 直接在父層寫 @update:prop="handler" 或使用 v-model:prop
事件參數過多 事件回呼函式變得難以閱讀 只傳遞必要的資料,若需要多個值可包成物件 { foo, bar }
未使用 defineEmits (Composition API) 失去編譯時類型檢查,容易拼寫錯誤 <script setup> 中使用 defineEmits(['eventName']),配合 IDE 提示

最佳實踐摘要

  1. Props 必須明確宣告:使用 typerequireddefault,讓組件自說明。
  2. 永遠不要直接改變 Props:若需要本地化修改,先 const local = ref(props.xxx)
  3. 事件名稱保持語意化:例如 update-countdelete-item,能讓閱讀者快速了解行為。
  4. 利用 v-model 簡化雙向綁定:適用於表單元件或需要同步父子狀態的情境。
  5. 適度使用 Provide/Inject:只在跨多層傳遞且不頻繁變更的資料時使用。

實際應用場景

1. 表單元件的雙向綁定

自訂 SelectBoxDatePicker 等表單元件時,使用 v-model(背後的 props + emit)能讓父層像原生 <input> 一樣簡潔。

2. 列表項目與父層狀態同步

在商品清單中,每個項目都有「加入購物車」按鈕。子項目透過 emit('add-to-cart', productId) 向父層回報,父層再更新全域購物車狀態(可配合 Pinia)。

3. 動態表格的欄位排序

父層提供資料陣列與排序方式(sortKeysortOrder)作為 props,子表格元件在表頭點擊時 emit('update:sortKey', newKey),父層即重新計算排序後的資料。

4. 多步驟表單(Wizard)

每一步都是子元件,父層持有整體表單資料。子元件在完成本步驟時 emit('next', stepData),父層合併資料並切換至下一步。


總結

  • PropsEmit 是 Vue3 中最核心的 單向資料流 機制,讓父子元件之間的溝通既直觀又可預測。
  • 正確宣告 props、避免直接修改、使用 defineEmits、以及遵循一致的事件命名規則,是避免常見錯誤的關鍵。
  • 在需要雙向綁定時,v-model 提供了簡潔的語法糖;而在跨多層傳遞時,適度採用 provide/inject 或全域狀態管理工具,可提升可維護性。
  • 透過上述概念與實作範例,你可以快速在 Vue3 專案中建立清晰的元件通信,從簡單的表單元件到複雜的多步驟流程,都能保持程式碼的可讀性與可測試性。

實務建議:在開發新元件前,先思考它的 資料來源(props)與 回饋方式(emit),再決定是否需要 v-model 或其他狀態管理手段。這樣的先行設計,將讓你的 Vue 應用在成長過程中更具彈性與可維護性。