Vue.js 组件 - 自定义事件 自定义事件是 Vue 子组件向父组件通信的核心机制,子组件通过emit触发事件,父组件通过@事件名监听并响应。
一、基本用法 1. 定义与触发 <!-- 子组件 ChildButton.vue --> <script setup> // 声明可触发的事件 const emit = defineEmits(['click', 'submit']) const handleClick = () => { emit('click') // 无参数 } const handleSubmit = () => { emit('submit', { // 携带参数 id: 1, name: 'Vue' }) } </script> <template> <button @click="handleClick">点击</button> <button @click="handleSubmit">提交</button> </template>2. 监听事件 <!-- 父组件 --> <template> <ChildButton @click="onClick" @submit="onSubmit" /> </template> <script setup> const onClick = () => { console.log('子组件被点击了') } const onSubmit = (data) => { console.log('提交数据:', data) // { id: 1, name: 'Vue' } } </script>二、defineEmits 详解 1. 数组语法(最常用) const emit= defineEmits ( [ 'update' , 'delete' , 'change' ] ) 2. 对象语法(带验证) const emit= defineEmits ( { // 无验证 click : null , // 带验证 — 返回 false 时控制台输出警告(事件仍会触发) submit : ( payload ) => { if ( ! payload. id) { console. warn ( 'submit 事件需要 id 字段' ) return false } return true } , // 多参数验证 update : ( id, newValue ) => { return typeof id=== 'number' && newValue!== undefined } } ) 3. TypeScript 类型声明 <script setup lang="ts"> const emit = defineEmits<{ (e: 'change', id: number): void (e: 'update', value: string): void (e: 'delete', id: number): void }>() // Vue 3.3+ 更简洁的写法 const emit = defineEmits<{ change: [id: number] update: [value: string] delete: [id: number] }>() </script>三、事件参数传递 1. 单个参数 <!-- 子组件 --> <script setup> const emit = defineEmits(['select']) const onSelect = (item) => { emit('select', item) } </script> <!-- 父组件 --> <Child @select="onSelect" /> <script setup> const onSelect = (item) => { console.log(item) // { id: 1, name: 'Vue' } } </script>2. 多个参数 <!-- 子组件 --> <script setup> const emit = defineEmits(['move']) const onMove = () => { emit('move', 100, 200) // 传递多个参数 } </script> <!-- 父组件 --> <Child @move="onMove" /> <script setup> const onMove = (x, y) => { console.log(x, y) // 100 200 } </script>3. 内联箭头函数提取参数 <!-- 只需要第二个参数时 --> <Child @move="(x, y) => currentY = y" />四、v-model 的自定义事件本质 1. 单个 v-model <!-- 父组件 --> <CustomInput v-model="value" /> <!-- 等价于 --> <CustomInput :modelValue="value" @update:modelValue="value = $event" /><!-- 子组件 CustomInput.vue --> <script setup> defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) </script> <template> <input :value="modelValue" @input="emit('update:modelValue', $event.target.value)" /> </template>2. 具名 v-model(多个双向绑定) <!-- 父组件 --> <UserForm v-model:name="userName" v-model:age="userAge" /> <!-- 等价于 --> <UserForm :name="userName" @update:name="userName = $event" :age="userAge" @update:age="userAge = $event" /><!-- 子组件 UserForm.vue --> <script setup> defineProps(['name', 'age']) const emit = defineEmits(['update:name', 'update:age']) </script> <template> <input :value="name" @input="emit('update:name', $event.target.value)" /> <input :value="age" @input="emit('update:age', Number($event.target.value))" /> </template>3. 自定义 v-model 修饰符 <!-- 父组件 --> <CustomInput v-model.capitalize="title" /><!-- 子组件 CustomInput.vue --> <script setup> const props = defineProps({ modelValue: String, modelModifiers: { default: () => ({}) } }) const emit = defineEmits(['update:modelValue']) const emitValue = (e) => { let value = e.target.value // 检查修饰符 if (props.modelModifiers.capitalize) { value = value.charAt(0).toUpperCase() + value.slice(1) } emit('update:modelValue', value) } </script> <template> <input :value="modelValue" @input="emitValue" /> </template>4. 具名 v-model 的修饰符 <!-- 父组件 --> <UserForm v-model:name.trim="userName" /><!-- 子组件 --> <script setup> const props = defineProps({ name: String, nameModifiers: { default: () => ({}) } // 命名规则:prop名 + Modifiers }) const emit = defineEmits(['update:name']) const handleInput = (e) => { let value = e.target.value if (props.nameModifiers.trim) { value = value.trim() } emit('update:name', value) } </script>五、事件命名规范 推荐使用 kebab-case // ✅ 推荐 const emit= defineEmits ( [ 'update-item' , 'delete-user' ] ) // ❌ 不推荐(HTML 不区分大小写,camelCase 会被转为小写导致监听失败) const emit= defineEmits ( [ 'updateItem' , 'deleteUser' ] ) <!-- 父组件监听 --> <Child @update-item="handler" /> <!-- ✅ --> <Child @updateItem="handler" /> <!-- ❌ 可能监听不到 -->常见事件命名约定 场景 事件名 说明 值更新 update:propNamev-model 约定 删除 delete/remove删除操作 变更 change值变化后触发 输入 input输入过程中触发 确认 confirm确认操作 取消 cancel取消操作 提交 submit表单提交
六、实战场景 1. 表单组件双向绑定 <!-- SearchBar.vue --> <script setup> const props = defineProps({ modelValue: String, placeholder: { type: String, default: '搜索...' } }) const emit = defineEmits(['update:modelValue', 'search']) const onInput = (e) => { emit('update:modelValue', e.target.value) } const onSearch = () => { emit('search', props.modelValue) } </script> <template> <div class="search-bar"> <input :value="modelValue" :placeholder="placeholder" @input="onInput" @keyup.enter="onSearch" /> <button @click="onSearch">搜索</button> </div> </template><!-- 父组件 --> <template> <SearchBar v-model="keyword" @search="doSearch" /> </template> <script setup> import { ref } from 'vue' const keyword = ref('') const doSearch = (val) => { console.log('搜索:', val) } </script>2. 列表项操作 <!-- TodoItem.vue --> <script setup> defineProps({ todo: Object }) const emit = defineEmits(['toggle', 'delete', 'edit']) const onToggle = () => emit('toggle', todo.id) const onDelete = () => emit('delete', todo.id) const onEdit = (e) => emit('edit', todo.id, e.target.value) </script> <template> <li :class="{ done: todo.done }"> <input type="checkbox" :checked="todo.done" @change="onToggle" /> <span>{{ todo.text }}</span> <button @click="onDelete">删除</button> </li> </template><!-- 父组件 --> <template> <TodoItem v-for="todo in todos" :key="todo.id" :todo="todo" @toggle="toggleTodo" @delete="deleteTodo" @edit="editTodo" /> </template> <script setup> const toggleTodo = (id) => { /* ... */ } const deleteTodo = (id) => { /* ... */ } const editTodo = (id, newText) => { /* ... */ } </script>3. 对话框组件 <!-- Dialog.vue --> <script setup> defineProps({ modelValue: Boolean, // 控制显示/隐藏 title: String }) const emit = defineEmits(['update:modelValue', 'confirm', 'cancel']) const close = () => emit('update:modelValue', false) const onConfirm = () => { emit('confirm') close() } const onCancel = () => { emit('cancel') close() } </script> <template> <div v-if="modelValue" class="dialog-overlay" @click.self="close"> <div class="dialog"> <h3>{{ title }}</h3> <slot></slot> <div class="dialog-footer"> <button @click="onCancel">取消</button> <button @click="onConfirm">确定</button> </div> </div> </div> </template><!-- 父组件 --> <template> <Dialog v-model="visible" title="确认删除" @confirm="onConfirm" @cancel="onCancel" > <p>确定要删除该项吗?</p> </Dialog> </template>七、注意事项 要点 说明 命名用 kebab-case emit('update-item')而非emit('updateItem'),避免大小写问题事件是单向的 emit只能向上传递,父组件监听是被动响应验证不阻止触发 defineEmits验证返回false仅输出警告,事件仍会触发避免修改 props 需要修改数据时用emit通知父组件,而非直接改 props v-model 事件名 必须是update:propName格式,这是 Vue 的约定 修饰符 prop modelModifiers/nameModifiers是 Vue 保留的 prop 名$event 位置 内联@event="handler($event)"中$event是事件回调的第一个参数