Vue3 Options API(傳統寫法)─ emits 完全指南
簡介
在 Vue3 中,Component 之間的溝通仍是開發大型 SPA 時最常碰到的需求。
父層傳資料給子層使用 props,子層回傳資訊給父層則透過 自訂事件(custom events),在 Options API 裡這個機制就是 emits。
即使 Vue3 引入了 Composition API,許多既有專案或是剛入門的開發者仍會選擇 Options API 來撰寫元件。掌握 emits 的正確寫法、驗證機制與最佳實踐,能讓元件的 可讀性、可維護性 大幅提升,避免因事件名稱拼寫錯誤或參數不符而產生難以追蹤的 bug。
本文將從概念說明、實作範例、常見陷阱到實務應用,完整呈現在 Options API 中使用 emits 的全貌,讓你能在日常開發中自信地運用自訂事件。
核心概念
1. 為什麼需要 emits?
在 Vue2 時,我們只需要在子元件內使用 this.$emit('eventName', payload),父層則在模板上寫 <Child @event-name="handler" />。
Vue3 為了 提升開發者體驗與 TypeScript 支援,在 Options API 中新增了 emits 選項:
export default {
// 明確宣告子元件會發出的事件名稱
emits: ['update', 'delete-item'],
// ...
}
- 事件驗證:Vue 會在開發模式下檢查
$emit的事件名稱是否已在emits中聲明,未聲明的事件會觸發警告。 - 自動推斷型別:配合 TypeScript 時,
emits能讓編譯器推斷事件的參數型別,減少型別錯誤。 - 文件化:閱讀元件時,一眼就能看出它會發出哪些事件,提升可讀性。
2. 基本語法
export default {
// 1️⃣ 宣告可發出的事件(陣列或物件)
emits: ['submit', 'cancel'],
methods: {
// 2️⃣ 使用 this.$emit 觸發事件
onSubmit() {
const data = { name: this.name, age: this.age };
this.$emit('submit', data); // 事件名稱必須與 emits 中一致
},
onCancel() {
this.$emit('cancel'); // 不傳參數也是合法的
},
},
};
Tip:若使用物件語法,可以同時提供驗證函式:
emits: {
// 事件名稱: 驗證函式,回傳 true 表示參數合法
submit: payload => typeof payload === 'object' && payload !== null,
cancel: () => true,
}
3. 搭配 .sync 修飾子
在 Options API 中,v-model 仍是透過 modelValue prop + update:modelValue 事件實作。若想自訂同步行為,只要在子元件 emits 中加入對應事件即可:
export default {
props: {
count: { type: Number, default: 0 },
},
emits: ['update:count'],
methods: {
increase() {
this.$emit('update:count', this.count + 1);
},
},
};
父層使用:
<Counter :count.sync="totalCount" />
Vue 會自動把 update:count 轉為 count 的同步更新。
4. 多參數與解構
$emit 可以一次傳遞多個參數,父層的事件處理函式會依序接收:
// 子元件
this.$emit('drag', startX, startY, { id: this.itemId });
<!-- 父層 -->
<Draggable @drag="handleDrag" />
methods: {
handleDrag(startX, startY, meta) {
console.log(startX, startY, meta.id);
},
}
注意:若使用物件語法驗證,驗證函式只能接收到第一個參數;若需要多參數驗證,建議改用陣列 + 手動檢查。
5. 事件名稱的命名慣例
- 使用 kebab-case(小寫加破折號)在模板中綁定事件:
@my-event="handler" - 在
emits與$emit中則建議使用 camelCase:'myEvent'
Vue 會自動在模板編譯時把my-event轉為myEvent。
// 子元件
emits: ['myEvent'],
methods: {
trigger() {
this.$emit('myEvent', 'payload');
},
}
<!-- 父層 -->
<Child @my-event="onMyEvent" />
程式碼範例
以下提供 五個實用範例,涵蓋從最基礎到稍微進階的 emits 用法。
範例 1:最簡單的自訂事件
// Child.vue
export default {
emits: ['hello'],
template: `<button @click="sayHello">Say Hello</button>`,
methods: {
sayHello() {
this.$emit('hello', '👋 來自子元件');
},
},
};
<!-- Parent.vue -->
<template>
<Child @hello="handleHello" />
</template>
<script>
import Child from './Child.vue';
export default {
components: { Child },
methods: {
handleHello(msg) {
alert(msg);
},
},
};
</script>
範例 2:使用物件語法驗證參數
// FormInput.vue
export default {
props: { modelValue: String },
emits: {
// 只接受字串且長度不超過 20
'update:modelValue': payload => typeof payload === 'string' && payload.length <= 20,
},
template: `
<input :value="modelValue" @input="onInput" />
`,
methods: {
onInput(e) {
this.$emit('update:modelValue', e.target.value);
},
},
};
開發模式 若輸入超過 20 個字,Vue 會在 console 警告:「Invalid payload ...」。
範例 3:同步 .sync 於自訂屬性
// Slider.vue
export default {
props: { value: Number },
emits: ['update:value'],
template: `
<input type="range"
:value="value"
@input="onSlide" />
`,
methods: {
onSlide(e) {
this.$emit('update:value', Number(e.target.value));
},
},
};
<!-- Parent.vue -->
<Slider :value.sync="volume" />
<p>目前音量:{{ volume }}</p>
範例 4:多參數事件 + 事件驗證
// DragBox.vue
export default {
emits: ['drag'],
template: `
<div class="box"
@mousedown="startDrag"
@mousemove="onMove"
@mouseup="stopDrag"></div>
`,
data() {
return { dragging: false, startX: 0, startY: 0 };
},
methods: {
startDrag(e) {
this.dragging = true;
this.startX = e.clientX;
this.startY = e.clientY;
},
onMove(e) {
if (!this.dragging) return;
const deltaX = e.clientX - this.startX;
const deltaY = e.clientY - this.startY;
// 觸發事件,傳遞三個參數
this.$emit('drag', deltaX, deltaY, { id: this.$attrs.id });
},
stopDrag() {
this.dragging = false;
},
},
};
// Parent.vue
methods: {
handleDrag(dx, dy, meta) {
console.log(`Box ${meta.id} 移動了 (${dx}, ${dy})`);
},
}
範例 5:在 TypeScript 中使用 emits(示範型別推斷)
// Counter.ts
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Counter',
props: {
modelValue: { type: Number, required: true },
},
emits: {
// 透過泛型宣告參數型別
'update:modelValue': (value: number) => typeof value === 'number',
},
setup(props, { emit }) {
const inc = () => emit('update:modelValue', props.modelValue + 1);
const dec = () => emit('update:modelValue', props.modelValue - 1);
return { inc, dec };
},
});
在父層使用時,IDE 會自動提示 update:modelValue 需要 number,減少錯誤。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記在 emits 中宣告 |
在開發模式會得到 event "xxx" was emitted but not declared 警告,且 TypeScript 無法推斷型別。 |
始終 在元件最上層寫 emits: [...](或物件),即使只有一個事件。 |
| 事件名稱大小寫不一致 | this.$emit('myEvent') 與 <Child @my-event> 會自動對應,但若在 $emit 使用 kebab-case,會失去自動轉換。 |
統一 在 $emit 使用 camelCase,模板使用 kebab-case。 |
| 多參數驗證失效 | 物件語法的驗證函式只能檢查第一個參數,其他參數不會被驗證。 | 若需要完整驗證,使用陣列方式宣告 emits,並在 methods 中自行檢查。 |
過度依賴 $emit 來傳遞大量資料 |
大量或深層物件每次變動都會觸發事件,可能導致效能問題。 | 只傳遞必要的 最小單位(例如 id),其餘資料可在父層自行查找或使用 Vuex / Pinia。 |
忘記在父層使用 .sync 或 v-model |
子元件發出 update:prop 事件卻未在父層綁定,導致資料不會同步。 |
確認 父層 已寫 :prop.sync="value" 或 v-model:prop="value"。 |
最佳實踐
- 宣告即驗證:盡量使用物件語法搭配驗證函式,讓錯誤在開發階段即被捕獲。
- 事件命名:使用動詞式名稱(
submit,close,update:count),讓事件意圖清晰。 - 保持單一職責:每個事件只負責一件事,避免在同一事件內同時傳遞「狀態變更」與「執行指令」。
- 文件化:在元件的 JSDoc 或 README 中列出
emits列表與參數說明,提升團隊協作效率。 - 測試:使用 Vue Test Utils 的
trigger與emitted斷言,確保事件正確發射與接收。
// 範例測試
import { mount } from '@vue/test-utils';
import Child from '@/components/Child.vue';
test('emit hello event with payload', async () => {
const wrapper = mount(Child);
await wrapper.find('button').trigger('click');
expect(wrapper.emitted()).toHaveProperty('hello');
expect(wrapper.emitted('hello')[0]).toEqual(['👋 來自子元件']);
});
實際應用場景
| 場景 | 使用 emits 的理由 |
|---|---|
表單子元件(如 Input, Select) |
透過 update:modelValue 讓父層使用 v-model,保持雙向綁定的直觀寫法。 |
| 彈窗 / 對話框 | 子元件在使用者點擊「確定」或「取消」時分別 $emit('confirm')、$emit('cancel'),父層負責真正的業務邏輯(如送出 API)。 |
| 拖拉排序(drag‑and‑drop) | 子元件在拖曳過程中發出 drag-start、drag-move、drag-end 事件,父層根據座標更新資料結構或觸發動畫。 |
| 圖表或圖形元件 | 使用 @point-click 傳遞點擊資料(如座標、資料索引),父層決定彈出資訊視窗或執行分析。 |
| 跨層級狀態同步(如多層級的折疊面板) | 子元件 $emit('toggle', id),父層收集所有子元件的開關狀態,統一管理展開/收合的邏輯。 |
透過上述範例,我們可以看到 emits 不僅是 事件傳遞 的管道,更是 元件介面設計 的一部分。妥善規劃與使用 emits,能讓元件保持 低耦合、高可測試性,在大型專案中減少維護成本。
總結
emits為 Vue3 Options API 中 宣告與驗證自訂事件 的核心機制。- 使用 陣列或物件語法 明確列出子元件會發出的事件,配合
$emit觸發,讓父層以@event-name方式監聽。 - 透過
.sync、v-model可實作雙向綁定;多參數、驗證函式、命名慣例 皆有最佳寫法。 - 常見錯誤包括忘記宣告、大小寫不一致與過度傳遞資料;最佳實踐則是 宣告即驗證、事件語意化、單一職責。
- 在表單、彈窗、拖曳、圖表等實務場景中,
emits為元件間溝通的橋樑,正確使用可提升程式碼的可讀性與可維護性。
掌握了上述概念與技巧後,你就能在 Vue3 的 Options API 中自如地使用 emits,寫出既 清晰 又 可靠 的元件,為你的前端專案奠定堅實的基礎。祝開發順利 🎉