本文 AI 產出,尚未審核

Vue3 Composition API 核心 – defineProps()defineEmits()

簡介

在 Vue 3 中,Composition API 讓我們以更靈活、可組合的方式撰寫元件邏輯。當使用 <script setup> 語法時,defineProps()defineEmits() 成為與父子元件溝通的兩大入口:前者用來 接收父層傳入的屬性,後者則用來 向父層發射事件

相較於 Vue 2 的 props$emit,這兩個函式在編譯階段即被 Vue 處理,能提供更好的型別推斷、Tree‑shaking 效果,並讓程式碼結構更為直觀。掌握它們的使用方式,是寫好可重用、可維護的 Vue 3 元件的關鍵。


核心概念

1. defineProps() 基本用法

defineProps() 會返回一個 只讀 的物件,裡面包含了父層傳入的所有屬性。使用方式有兩種:

  1. 字面量型別推斷(不需要額外的 TypeScript 定義)
  2. 型別介面(interface)或型別別名(type),配合 TypeScript 获得更完整的型別檢查。
<script setup>
// 方式一:直接寫字面量
const props = defineProps({
  title: String,
  count: {
    type: Number,
    default: 0
  }
})

// 方式二:使用 TypeScript 介面
interface CardProps {
  title: string
  count?: number   // 可選屬性
}
const props = defineProps<CardProps>()
</script>

重點defineProps() 回傳的 props只讀 的,若直接修改會在開發環境拋出警告。若需要衍生出可變的資料,請使用 refcomputed 包裝。


2. defineEmits() 基本用法

defineEmits() 用來宣告元件會 發射哪些事件,同樣支援字面量與 TypeScript 型別兩種寫法。宣告後即可直接呼叫回傳的 emit 函式。

<script setup>
// 方式一:字面量宣告
const emit = defineEmits(['update', 'delete'])

// 方式二:型別宣告(支援參數型別檢查)
type CardEmits = {
  (e: 'update', payload: number): void
  (e: 'delete'): void
}
const emit = defineEmits<CardEmits>()
</script>

呼叫方式:

// 在某個事件處理器內發射
function onIncrement() {
  emit('update', props.count + 1)   // payload 必須符合型別
}
function onRemove() {
  emit('delete')
}

提示:在 <script setup> 中,emit 只在本檔案可見,外部仍然是透過 v-on@ 綁定事件。


3. 結合 defineProps()defineEmits() 的完整範例

以下是一個簡易的 計數卡片(CounterCard)元件,示範如何同時使用 defineProps()defineEmits()

<script setup>
/* Props 定義 */
interface CounterProps {
  /** 卡片標題 */
  title: string
  /** 初始值,預設為 0 */
  initial?: number
}
const props = defineProps<CounterProps>()
const count = ref(props.initial ?? 0)

/* Emits 定義 */
type CounterEmits = {
  (e: 'change', newValue: number): void
}
const emit = defineEmits<CounterEmits>()

/* 方法 */
function increment() {
  count.value++
  emit('change', count.value)   // 立即回報新值
}
function decrement() {
  if (count.value > 0) {
    count.value--
    emit('change', count.value)
  }
}
</script>

<template>
  <div class="counter-card">
    <h3>{{ props.title }}</h3>
    <p>Count: {{ count }}</p>
    <button @click="decrement">-</button>
    <button @click="increment">+</button>
  </div>
</template>

<style scoped>
.counter-card { border: 1px solid #ddd; padding: 1rem; border-radius: 4px; }
button { margin: 0 0.5rem; }
</style>

說明

  • props.title 直接取自 defineProps(),且因為是 readonly,不會被誤改。
  • count 為本地的 ref,可自由變更。
  • 每次計數變動,都會透過 emit('change', count.value) 把最新值傳回父層,父層可用 @change="handleChange" 監聽。

4. 動態 Props 與 Event 名稱

有時候我們想讓元件接受 動態屬性(如 v-bind="[obj]")或 動態事件名稱(如 v-on="[eventObj]")。defineProps() 允許使用 索引簽名(index signature)來接受任意鍵值:

<script setup lang="ts">
interface AnyProps {
  [key: string]: any   // 接受任何屬性
}
const props = defineProps<AnyProps>()
</script>

同理,defineEmits() 也可以接受字串陣列或泛型函式簽名,讓 任意事件 都能被正確推斷:

<script setup lang="ts">
type AnyEmits = (event: string, ...args: any[]) => void
const emit = defineEmits<AnyEmits>()
emit('custom-event', { foo: 'bar' })
</script>

注意:使用索引簽名會失去編譯期的嚴格檢查,僅在真的需要「完全開放」的情況下才建議使用。


5. 在普通 <script> 中使用 defineProps / defineEmits

如果不想使用 <script setup>,仍可在普通 <script> 中透過 setup() 函式取得同樣的功能:

<script>
export default {
  props: {
    title: String,
    value: Number
  },
  emits: ['update'],
  setup(props, { emit }) {
    // props 為只讀物件,emit 為發射函式
    function change(val) {
      emit('update', val)
    }
    return { change }
  }
}
</script>

這裡的 propsemitdefineProps()defineEmits() 的行為相同,只是寫法較為冗長。建議在新專案或新元件中優先採用 <script setup>


常見陷阱與最佳實踐

陷阱 說明 解決方式
直接修改 props defineProps() 回傳的物件是唯讀的,直接賦值會觸發 Vue 警告。 使用 refreactivecomputed 包裝,或在 setup 中建立本地變數。
忘記宣告 emits 未在 defineEmits() 中列出事件,編譯器不會提供型別檢查,且在 v-on 時不會自動提示。 必須 使用 defineEmits() 明確列出所有會發射的事件。
事件名稱拼寫不一致 父層使用 @my-event,子層卻發射 emit('myEvent')(大小寫不同),會導致事件無法被捕獲。 統一使用 kebab-casemy-event)或 camelCasemyEvent),並在 defineEmits 中保持一致。
Payload 型別不匹配(TS) emit('update', 'string') 卻在 defineEmits 中宣告為 number,會在編譯階段報錯。 利用 TypeScript 的函式簽名或字面量陣列,讓 IDE 即時提示錯誤。
過度使用 any 為了省事把所有 Props/Emits 設為 any,失去型別安全。 只在真的需要動態屬性時才使用索引簽名,其餘情況應明確定義每個屬性與事件。

最佳實踐

  1. 盡量使用 TypeScriptdefineProps<T>()defineEmits<T>() 能提供完整的型別推斷,減少執行時錯誤。
  2. 保持單一職責:每個元件的 props 應只描述「外部可設定的資料」,emits 只描述「外部可訂閱的行為」。不要在同一元件內混合過多的邏輯。
  3. 使用 readonly 斷言:若真的需要在內部暫時修改 props,可使用 const mutable = ref(props.someKey as unknown as number),但必須確保不會影響父層狀態。
  4. 事件名稱使用 kebab-case:Vue 在模板中自動把 kebab-case 轉為 camelCase,遵守此慣例可避免不必要的錯誤。
  5. 在測試中驗證 Emits:使用 @vue/test-utilsemitted() 方法確認事件是否正確發射,確保元件行為符合預期。

實際應用場景

1. 表單元件(Input、Select)

表單元件常需要把 使用者輸入的值v-model 形式回傳父層。使用 defineProps 接收 modelValuedefineEmits 發射 update:modelValue

<script setup lang="ts">
const props = defineProps<{
  modelValue: string
}>()
const emit = defineEmits<{
  (e: 'update:modelValue', v: string): void
}>()

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

<template>
  <input :value="props.modelValue" @input="onInput" />
</template>

2. 資料表格子元件(Row、Cell)

父層傳入每列資料 (rowData),子元件在點擊時發射 select 事件,讓父層決定如何處理:

<script setup>
const props = defineProps<{ rowData: Record<string, any> }>()
const emit = defineEmits(['select'])

function onClick() {
  emit('select', props.rowData)
}
</script>

<template>
  <tr @click="onClick">
    <td v-for="(val, key) in props.rowData" :key="key">{{ val }}</td>
  </tr>
</template>

3. 動態 UI 組件(Modal、Drawer)

Modal 元件接受 visible prop,並在點擊關閉按鈕時發射 close 事件:

<script setup>
const props = defineProps<{ visible: boolean }>()
const emit = defineEmits(['close'])

function close() {
  emit('close')
}
</script>

<template>
  <Teleport to="body" v-if="props.visible">
    <div class="overlay" @click.self="close">
      <div class="modal">
        <slot />
        <button @click="close">關閉</button>
      </div>
    </div>
  </Teleport>
</template>

這樣父層只需要 v-model:visible@close 即可控制顯示與隱藏,邏輯與 UI 完全解耦


總結

  • defineProps()defineEmits() 是 Vue 3 <script setup> 中與父層溝通的核心工具,提供 只讀 Props型別安全的事件發射
  • 使用 TypeScript 能最大化它們的優勢:自動補全、編譯期錯誤檢查、文件生成。
  • 常見錯誤包括直接修改 props、忘記宣告 emits、事件名稱不一致等,遵守最佳實踐可有效避免。
  • 在表單、資料表格、彈窗等常見 UI 元件中,defineProps/defineEmits 能讓元件保持 單向資料流高度可重用,同時提升開發效率與可維護性。

掌握這兩個 API,您就能在 Vue 3 中寫出更乾淨、可組合、且易於測試的元件,為大型專案奠定堅實的基礎。祝開發順利 🎉!