本文 AI 產出,尚未審核

Vue3 元件基礎:emit 事件回傳與參數


簡介

在 Vue3 的組件化開發流程中,父子元件之間的 資料傳遞 是最常見也最重要的需求之一。雖然 props 讓父層可以把資料「推」給子層,但若要讓子層把使用者的操作或計算結果「回傳」給父層,就必須透過 自訂事件 (emit) 來完成。

emit 不僅是單純的訊號,還可以攜帶 任意數量與型別的參數,讓父層可以依據這些資訊執行對應的邏輯。掌握 emit 的正確寫法與最佳實踐,能讓你的 Vue 專案在可讀性、維護性與擴充性上都有大幅提升。

本篇文章將從概念說明、實作範例、常見陷阱到實務應用,完整呈現 Vue3 中 emit 事件回傳與參數 的全貌,適合初學者快速上手,也能讓中級開發者檢視自己的寫法是否符合最佳實踐。


核心概念

1. 為什麼需要 emit

  • 單向資料流:Vue 推崇「父傳子、子回傳」的單向資料流,props 用於「向下」傳遞,emit 用於「向上」回傳。
  • 解耦合:子元件不需要知道父元件的實作細節,只要觸發約定好的事件名稱即可。
  • 可重用:同一個子元件可以在多個父元件中使用,只要父元件監聽相同的事件,就能得到相同的回傳資料。

2. 基本語法

子元件 中使用 defineEmits(Composition API)或 $emit(Options API)來宣告與觸發事件:

// Composition API
import { defineComponent, defineEmits } from 'vue'

export default defineComponent({
  setup(_, { emit }) {
    const clickHandler = () => {
      // 觸發名為 'submit' 的事件,並傳遞兩個參數
      emit('submit', { name: 'Alice' }, 42)
    }

    return { clickHandler }
  }
})
// Options API
export default {
  methods: {
    clickHandler() {
      // 觸發 'submit' 事件
      this.$emit('submit', { name: 'Bob' }, 42)
    }
  }
}

父元件 中使用 v-on(或簡寫 @)監聽:

<ChildComponent @submit="handleSubmit" />
export default {
  methods: {
    handleSubmit(payload, count) {
      console.log('收到子元件回傳:', payload, count)
    }
  }
}

3. 事件名稱的命名慣例

命名風格 推薦寫法 說明
kebab-case @my-event Vue 在模板中會自動將 kebab-case 轉成 camelCase,最常見且兼容 HTML 標準。
camelCase @myEvent 只能在 JSXrender function 中使用。
PascalCase @MyEvent 不建議,易與元件名稱混淆。

建議:在模板裡統一使用 kebab-case,保持可讀性與一致性。

4. 參數傳遞的技巧

  1. 單一物件:把多個欄位包在一個物件裡,讓 API 更具可擴充性。
    emit('update', { id: 1, title: '新標題', completed: false })
    
  2. 多參數:若事件意義明確且參數固定,可直接傳多個參數。
    emit('page-change', newPage, pageSize)
    
  3. 回傳 Promise:子元件也可以透過 emit 回傳 Promise,讓父元件使用 await 處理非同步流程。
    // 子元件
    emit('save', formData, (resolve, reject) => {
      // 這裡是子元件內部的非同步操作
    })
    

5. TypeScript 中的型別定義

使用 defineEmits 時可以提供型別,讓 IDE 具備自動補全與錯誤檢查:

import { defineComponent, defineEmits } from 'vue'

type SubmitPayload = { name: string; age: number }

export default defineComponent({
  emits: {
    // 事件名稱: 參數驗證函式(可省略,僅作型別宣告)
    submit: (payload: SubmitPayload, count: number) => true
  },
  setup(_, { emit }) {
    const send = () => {
      const data: SubmitPayload = { name: 'Cathy', age: 28 }
      emit('submit', data, 1)
    }
    return { send }
  }
})

程式碼範例

下面提供 5 個實用範例,逐步展示 emit 的不同寫法與應用情境。每個範例皆附上說明與常見注意事項。

範例 1:最簡單的按鈕點擊回傳

<!-- Parent.vue -->
<template>
  <ChildBtn @clicked="onChildClicked" />
</template>

<script setup>
import ChildBtn from './ChildBtn.vue'

function onChildClicked() {
  alert('子元件的按鈕被點了!')
}
</script>
// ChildBtn.vue (Composition API)
<script setup>
import { defineEmits } from 'vue'

const emit = defineEmits(['clicked'])

function handleClick() {
  // 只傳遞事件名稱,無參數
  emit('clicked')
}
</script>

<template>
  <button @click="handleClick">點我</button>
</template>

重點:即使不帶參數,也必須在 defineEmits 中列出事件名稱,讓 Vue 能正確檢查。


範例 2:傳遞單一物件作為參數

<!-- Parent.vue -->
<template>
  <UserForm @submit="handleFormSubmit" />
</template>

<script setup>
import UserForm from './UserForm.vue'

function handleFormSubmit(data) {
  console.log('表單資料:', data)
}
</script>
// UserForm.vue (Options API)
<template>
  <form @submit.prevent="onSubmit">
    <input v-model="name" placeholder="姓名" />
    <input v-model.number="age" type="number" placeholder="年齡" />
    <button type="submit">送出</button>
  </form>
</template>

<script>
export default {
  data() {
    return { name: '', age: null }
  },
  methods: {
    onSubmit() {
      // 把所有欄位包成一個物件
      this.$emit('submit', { name: this.name, age: this.age })
    }
  }
}
</script>

技巧:使用 單一物件 可以在未來新增欄位時,只要在子元件內部補上屬性,父層的接收程式碼不需要變更。


範例 3:多參數 + 型別驗證(Composition API + TypeScript)

<!-- Parent.vue -->
<template>
  <Pagination @page-change="onPageChange" />
</template>

<script setup lang="ts">
import Pagination from './Pagination.vue'

function onPageChange(page: number, size: number) {
  console.log(`切換至第 ${page} 頁,每頁 ${size} 筆`)
}
</script>
// Pagination.vue
<script setup lang="ts">
import { defineEmits, ref } from 'vue'

const emit = defineEmits<{
  (e: 'page-change', page: number, size: number): void
}>()

const currentPage = ref(1)
const pageSize = ref(10)

function goTo(page: number) {
  currentPage.value = page
  emit('page-change', page, pageSize.value)
}
</script>

<template>
  <button @click="goTo(currentPage - 1)" :disabled="currentPage === 1">上一頁</button>
  <span>第 {{ currentPage }} 頁</span>
  <button @click="goTo(currentPage + 1)">下一頁</button>
</template>

重點:使用 defineEmits 的泛型寫法,可在編譯階段捕捉錯誤,避免傳錯參數型別。


範例 4:非同步回傳(Promise)

<!-- Parent.vue -->
<template>
  <AsyncUploader @upload="handleUpload" />
</template>

<script setup>
import AsyncUploader from './AsyncUploader.vue'

async function handleUpload(file) {
  try {
    const url = await uploadToServer(file)   // 假設有此 API
    console.log('上傳成功,檔案網址:', url)
  } catch (e) {
    console.error('上傳失敗', e)
  }
}
</script>
// AsyncUploader.vue
<script setup>
import { defineEmits } from 'vue'

const emit = defineEmits(['upload'])

function onFileChange(event) {
  const file = event.target.files[0]
  // 直接把檔案物件 emit 給父層,父層自行處理上傳
  emit('upload', file)
}
</script>

<template>
  <input type="file" @change="onFileChange" />
</template>

說明emit 本身不會回傳 Promise,這裡的非同步處理是 父層 完成的。子層只負責 傳遞資料,保持職責單一。


範例 5:使用 v-model 自訂事件(雙向綁定)

Vue3 允許自訂 v-modelmodelValueupdate:modelValue 事件,實際上就是 emit 的應用:

<!-- Parent.vue -->
<template>
  <CustomSwitch v-model="isOn" />
  <p>開關狀態:{{ isOn }}</p>
</template>

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

const isOn = ref(false)
</script>
// CustomSwitch.vue
<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  modelValue: { type: Boolean, default: false }
})
const emit = defineEmits(['update:modelValue'])

function toggle() {
  emit('update:modelValue', !props.modelValue)
}
</script>

<template>
  <button @click="toggle">
    {{ props.modelValue ? 'ON' : 'OFF' }}
  </button>
</template>

關鍵v-model 背後等同於 :modelValue="..." @update:modelValue="...",只要正確 emit update:modelValue,雙向綁定即可順利運作。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記在 defineEmits / emits 中宣告事件 Vue 會在開發模式下警告「未宣告的事件」 在子元件的 emits(Options API)或 defineEmits(Composition API)裡列出所有會被觸發的事件名稱
事件名稱大小寫不一致 在模板使用 kebab-case,在 JS 使用 camelCase,若混用會導致監聽失敗 統一:模板寫 @my-event,JS 中 emit('myEvent')
過度傳遞多個參數 父層接收者需要記住參數順序,易出錯且不易維護 優先使用單一物件,將相關資訊封裝;若多參數是固定且語意清晰,才使用
在子元件內部直接修改 props props 為只讀,會觸發 Vue 警告 若需要本地化修改,先 const local = ref(props.xxx),或使用 emit 回傳變更
忘記 event.preventDefault() 在表單或按鈕事件裡未阻止預設行為,會導致頁面重新載入 在模板上使用 @submit.prevent 或在方法內手動 event.preventDefault()
父層忘記監聽事件 子元件 emit 正常,但父層無回應 確認父層模板有正確的 @event-name="handler",或檢查是否使用了 v-on="..."動態事件
非同步錯誤未捕獲 父層在 await emit(其實不會)或在回調中拋錯,會導致未處理的例外 父層處理非同步時自行 try/catch,子元件僅負責 emit,不必包裝 Promise

最佳實踐

  1. 明確的事件命名:使用動詞開頭,如 update, delete, submit,讓事件意圖一目了然。
  2. 一次只傳遞一個「意圖」:若要回傳「成功」與「失敗」兩種結果,建議分別使用 successerror 兩個事件,而非在同一事件內傳布林值。
  3. 使用 TypeScript 時提供完整型別:能在開發階段即捕捉錯誤,減少 runtime bug。
  4. 保持子元件的「純粹」:子元件只負責 UI 與 emit,不應該直接呼叫 API;讓父層或 Vuex/Pinia 處理業務邏輯。
  5. 文件化自訂事件:在元件說明檔(README、Storybook)中列出所有 emit 事件與參數結構,提升團隊協作效率。

實際應用場景

1. 表單元件的即時驗證

  • 子元件<InputField> 內部監聽 input,每次變更 emit('validate', { value, valid })
  • 父元件:收集所有欄位的驗證結果,決定「送出」按鈕是否啟用。

2. 列表項目操作(編輯 / 刪除)

  • 子元件<TodoItem> 內部提供「編輯」與「刪除」按鈕,分別 emit('edit', item)emit('remove', item.id)
  • 父元件:集中管理 Todo 列表,根據事件更新資料來源(Vuex/Pinia)或呼叫後端 API。

3. 複雜 UI 元件的雙向綁定

  • 自訂 v-model:如上面的 CustomSwitchDatePickerRichTextEditor,皆透過 emit('update:modelValue', newValue) 讓父層的資料自動同步。

4. 子元件觸發全局通知

  • 子元件<FileUploader> 完成上傳後 emit('complete', fileInfo)
  • 父層或全局事件總線:接收到 complete 後,呼叫全局的 toast/notification 系統顯示「上傳成功」訊息。

5. 多層級嵌套的事件傳遞

  • 情境GrandParent -> Parent -> Child
  • 做法:子層 emit('action', payload),父層若不直接處理,可 重新 emit 給上層:
    // Parent.vue
    <Child @action="forwardAction" />
    methods: {
      forwardAction(payload) {
        this.$emit('action', payload) // 轉發給 GrandParent
      }
    }
    
  • 此技巧讓 中間層保持薄弱,只負責「轉發」或「簡單處理」即可。

總結

  • emit 是 Vue3 中 子元件向父元件回傳資訊 的核心機制,搭配 props 完成完整的 單向資料流
  • 透過 正確的事件命名、參數封裝與型別宣告,可以讓元件間的溝通更清晰、維護更容易。
  • 常見的陷阱(忘記宣告事件、大小寫不一致、過度傳遞參數)只要遵守 最佳實踐(單一物件參數、使用 TypeScript、保持子元件純粹)即可避免。
  • 在實務開發中,emit 廣泛應用於表單驗證、列表操作、雙向綁定、全局通知與多層級事件轉發等場景,是打造可重用、可擴充 Vue 元件的關鍵工具。

掌握了 emit 的使用方式與注意事項,你就能在 Vue3 專案中更自信地構建 乾淨、可維護、具備良好可讀性的元件,為前端開發的品質與效率奠定堅實基礎。祝你開發順利 🚀