Vue 事件處理與 Emit
在 Vue 中,元件可以透過事件(Events)與父元件進行通訊。子元件發送事件,父元件監聽並處理這些事件,這是實現子傳父通訊的主要方式。
監聽事件
使用 v-on 指令(簡寫 @)來監聯 DOM 事件:
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<!-- 完整語法 -->
<button v-on:click="increment">+1</button>
<!-- 簡寫語法(推薦) -->
<button @click="increment">+1</button>
<!-- 內聯表達式 -->
<button @click="count++">+1</button>
<p>計數:{{ count }}</p>
</template>
方法處理器 vs 內聯處理器
方法處理器
當事件處理邏輯較複雜時,建議使用方法處理器:
<script setup>
function handleClick(event) {
// event 是原生 DOM 事件
console.log(event.target.tagName)
}
</script>
<template>
<button @click="handleClick">點擊</button>
</template>
內聯處理器
簡單的操作可以直接寫在模板中:
<script setup>
import { ref } from 'vue'
const count = ref(0)
function say(message) {
alert(message)
}
</script>
<template>
<button @click="count++">+1</button>
<button @click="say('Hello')">打招呼</button>
</template>
在內聯處理器中存取事件物件
<script setup>
function handleClick(message, event) {
console.log(message)
console.log(event.target)
}
</script>
<template>
<!-- 使用特殊變數 $event -->
<button @click="handleClick('Hello', $event)">點擊</button>
<!-- 使用箭頭函式 -->
<button @click="(event) => handleClick('Hello', event)">點擊</button>
</template>
定義元件事件
子元件使用 defineEmits() 來定義可以發送的事件:
陣列語法
<!-- ChildComponent.vue -->
<script setup>
// 定義可發送的事件
const emit = defineEmits(['submit', 'cancel'])
function handleSubmit() {
emit('submit')
}
function handleCancel() {
emit('cancel')
}
</script>
<template>
<button @click="handleSubmit">送出</button>
<button @click="handleCancel">取消</button>
</template>
物件語法(帶驗證)
<script setup>
const emit = defineEmits({
// 沒有驗證
click: null,
// 有驗證函式
submit: (payload) => {
if (payload.email && payload.password) {
return true
}
console.warn('Invalid submit event payload!')
return false
}
})
function handleSubmit() {
emit('submit', { email: 'test@example.com', password: '123456' })
}
</script>
監聽元件事件
父元件使用 @ 來監聽子元件發送的事件:
<!-- ParentComponent.vue -->
<script setup>
import ChildComponent from './ChildComponent.vue'
function onSubmit() {
console.log('子元件發送了 submit 事件')
}
function onCancel() {
console.log('子元件發送了 cancel 事件')
}
</script>
<template>
<ChildComponent
@submit="onSubmit"
@cancel="onCancel"
/>
</template>
事件傳遞參數
子元件可以在發送事件時傳遞資料:
<!-- ChildComponent.vue -->
<script setup>
import { ref } from 'vue'
const emit = defineEmits(['update'])
const inputValue = ref('')
function handleUpdate() {
// 第二個參數起是傳遞的資料
emit('update', inputValue.value)
}
</script>
<template>
<input v-model="inputValue">
<button @click="handleUpdate">更新</button>
</template>
<!-- ParentComponent.vue -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const message = ref('')
function handleUpdate(value) {
// value 是子元件傳來的資料
message.value = value
}
</script>
<template>
<ChildComponent @update="handleUpdate" />
<p>接收到的值:{{ message }}</p>
<!-- 也可以直接內聯 -->
<ChildComponent @update="(val) => message = val" />
</template>
傳遞多個參數
<!-- 子元件 -->
<script setup>
const emit = defineEmits(['submit'])
function handleSubmit() {
emit('submit', 'John', 'john@example.com', { role: 'admin' })
}
</script>
<!-- 父元件 -->
<script setup>
function onSubmit(name, email, meta) {
console.log(name, email, meta)
}
</script>
<template>
<ChildComponent @submit="onSubmit" />
</template>
事件命名慣例
事件名稱推薦使用 kebab-case:
<!-- 子元件 -->
<script setup>
const emit = defineEmits(['update-user', 'delete-user'])
</script>
<!-- 父元件 -->
<template>
<UserCard
@update-user="handleUpdate"
@delete-user="handleDelete"
/>
</template>
使用 TypeScript 定義事件
<script setup lang="ts">
// 方式一:使用泛型
const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'submit', email: string, password: string): void
}>()
// 方式二:Vue 3.3+ 更簡潔的語法
const emit = defineEmits<{
update: [value: string]
submit: [email: string, password: string]
}>()
</script>
實際範例
計數器元件
<!-- Counter.vue -->
<script setup>
defineProps({
count: {
type: Number,
default: 0
}
})
const emit = defineEmits(['increment', 'decrement', 'reset'])
</script>
<template>
<div class="counter">
<button @click="emit('decrement')">-</button>
<span>{{ count }}</span>
<button @click="emit('increment')">+</button>
<button @click="emit('reset')">重置</button>
</div>
</template>
<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import Counter from './Counter.vue'
const count = ref(0)
</script>
<template>
<Counter
:count="count"
@increment="count++"
@decrement="count--"
@reset="count = 0"
/>
</template>
搜尋框元件
<!-- SearchBox.vue -->
<script setup>
import { ref } from 'vue'
const emit = defineEmits(['search', 'clear'])
const query = ref('')
function handleSearch() {
if (query.value.trim()) {
emit('search', query.value.trim())
}
}
function handleClear() {
query.value = ''
emit('clear')
}
</script>
<template>
<div class="search-box">
<input
v-model="query"
type="text"
placeholder="請輸入搜尋關鍵字"
@keyup.enter="handleSearch"
>
<button @click="handleSearch">搜尋</button>
<button @click="handleClear" v-if="query">清除</button>
</div>
</template>
<style scoped>
.search-box {
display: flex;
gap: 10px;
}
input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
flex: 1;
}
button {
padding: 8px 16px;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #3aa876;
}
</style>
<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import SearchBox from './SearchBox.vue'
const searchResults = ref([])
async function handleSearch(query) {
console.log('搜尋關鍵字:', query)
// 執行搜尋邏輯...
}
function handleClear() {
searchResults.value = []
}
</script>
<template>
<SearchBox @search="handleSearch" @clear="handleClear" />
</template>
確認對話框元件
<!-- ConfirmDialog.vue -->
<script setup>
defineProps({
title: {
type: String,
default: '確認'
},
message: {
type: String,
required: true
},
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['confirm', 'cancel', 'update:visible'])
function handleConfirm() {
emit('confirm')
emit('update:visible', false)
}
function handleCancel() {
emit('cancel')
emit('update:visible', false)
}
</script>
<template>
<div v-if="visible" class="dialog-overlay" @click.self="handleCancel">
<div class="dialog">
<h3>{{ title }}</h3>
<p>{{ message }}</p>
<div class="actions">
<button class="cancel" @click="handleCancel">取消</button>
<button class="confirm" @click="handleConfirm">確認</button>
</div>
</div>
</div>
</template>
<style scoped>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.dialog {
background: white;
padding: 20px;
border-radius: 8px;
min-width: 300px;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.cancel {
background-color: #ddd;
}
.confirm {
background-color: #42b883;
color: white;
}
</style>
<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import ConfirmDialog from './ConfirmDialog.vue'
const showDialog = ref(false)
function handleConfirm() {
console.log('使用者確認了操作')
// 執行刪除等操作...
}
function handleCancel() {
console.log('使用者取消了操作')
}
</script>
<template>
<button @click="showDialog = true">刪除項目</button>
<ConfirmDialog
v-model:visible="showDialog"
title="刪除確認"
message="確定要刪除這個項目嗎?此操作無法復原。"
@confirm="handleConfirm"
@cancel="handleCancel"
/>
</template>
原生事件 vs 元件事件
在元件上使用 @click 等原生事件名稱時,預設監聽的是元件發出的自訂事件,而不是原生 DOM 事件。
如果想要監聽原生事件,需要在元件內部自己處理,或使用 v-on 的 .native 修飾符(Vue 3 已移除此修飾符)。
在 Vue 3 中,如果元件有單一根元素,未被 defineEmits 聲明的事件會自動「穿透」到根元素:
<!-- MyButton.vue -->
<script setup>
// 沒有聲明 click 事件
</script>
<template>
<button><slot /></button>
</template>
<!-- 父元件 -->
<template>
<!-- 這個 @click 會自動綁定到 button 元素上 -->
<MyButton @click="handleClick">點我</MyButton>
</template>
如果不想要這種行為,可以使用 defineOptions 禁用繼承:
<script setup>
defineOptions({
inheritAttrs: false
})
</script>