在 Vue 的组件通信模型中,Props 负责父组件向子组件传递数据,而 Emits 负责子组件向父组件发送消息。它遵循单向数据流原则:子组件不能直接修改父组件传入的 Props,但可以通过触发自定义事件,让父组件自己决定如何响应变化。
子组件:声明与触发事件
Vue 3 推荐使用 <script setup> 语法糖与 defineEmits 宏来声明组件可触发的事件。这既是给父组件的“使用说明书”,也能在编辑器中提供类型提示。
<!-- ChildCounter.vue -->
<template>
<div>
<p>当前计数:{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script setup>
// 1. 定义组件的 props
const props = defineProps({
count: Number
})
// 2. 声明可触发的事件及其参数类型
const emit = defineEmits(['update:count'])
// 3. 触发事件,向上传递新值
function increment() {
emit('update:count', props.count + 1)
}
</script>
defineEmits 接收一个字符串数组,列出所有可用的事件名。调用 emit('事件名', 参数1, 参数2, ...) 即可通知父组件,后续参数都会被传递给父组件的监听函数。
父组件:监听事件并处理
父组件在模板中使用 v-on(简写 @)来监听子组件抛出的自定义事件,就像监听原生 DOM 事件一样。
<!-- ParentPage.vue -->
<template>
<ChildCounter :count="counter" @update:count="handleCountUpdate" />
</template>
<script setup>
import { ref } from 'vue'
import ChildCounter from './ChildCounter.vue'
const counter = ref(0)
function handleCountUpdate(newValue) {
counter.value = newValue // 父组件负责修改数据,再通过 props 传回子组件
}
</script>
事件处理函数接收子组件传递的参数(这里是 newValue),然后更新父组件自身的状态。这保证了数据的所有权始终在父组件,子组件只是“建议”父组件做出更新,符合单向数据流的清晰结构。
带参数的复杂场景
事件可以携带多个参数,比如一个表单组件在提交时传递整个表单对象:
<!-- ChildForm.vue -->
<template>
<form @submit.prevent="submit">
<input v-model="name" />
<input v-model="email" />
<button type="submit">提交</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
const name = ref('')
const email = ref('')
const emit = defineEmits(['submit-form'])
function submit() {
emit('submit-form', { name: name.value, email: email.value })
}
</script>
父组件监听时可以直接解构参数:
<ChildForm @submit-form="handleFormSubmit" />
function handleFormSubmit(formData) {
console.log('收到表单:', formData.name, formData.email)
}
TypeScript 支持:给事件加类型
使用 TypeScript 时,defineEmits 可以接收类型参数,为事件及其负载提供严格的类型校验:
const emit = defineEmits<{
(e: 'update:count', value: number): void
(e: 'submit-form', data: { name: string; email: string }): void
}>()
这样在触发事件时,参数类型和数量会得到 IDE 提示和编译检查;父组件在监听事件时也能自动推断处理函数的参数类型。
实际开发中的常见模式
- v-model 的本质
稍后会详细讲到 v-model 是 Props + Emits 的语法糖。一个 <Child v-model:count="parentCount" /> 会被展开成 :count="parentCount" @update:count="parentCount = $event"。Emits 正是 update:xx 系列自定义事件的基础。
- “状态提升”的实现载体
当两个同级组件需要通信时,通常将共享状态提取到最近的公共父组件,然后通过 Props 下发给组件 A,通过 Emits 接收组件 B 的更新通知——Emits 是这种模式不可或缺的一环。
- 与原生事件区分
自定义事件名建议使用小写字母加连字符(如 submit-form),以避免与原生 DOM 事件冲突。Vue 3 移除了 v-on 的 .native 修饰符,所有事件监听都需要显式声明;如需监听组件根元素的原生事件,可在子组件内通过 inheritAttrs: false + 手动绑定来暴露。
避开常见的坑
- 不要在
emit时修改 props
子组件中直接修改 props.xxx 会引发 Vue 警告,且无法让数据同步回父组件。正确的做法永远是 emit 一个新值,让父组件更新自己的数据后再通过 props 回传。
- 事件名大小写
虽然 DOM 模板中事件名会被自动转为小写,但习惯上在 JavaScript 中定义事件时使用 camelCase(如 update:count),在模板中使用 kebab-case(@update:count)。Vue 会自动映射,所以保持一致即可。
- 忘记声明 emit
如果不在 defineEmits 中声明事件,组件依然可以触发事件,但会触发控制台警告:“Component emitted event xxx but it is neither declared...”。声明不仅是规范,也是获得类型提示和未来兼容性检查的依据。
说到底,Emits 是一个简单但关键的“上行通道”,它让父子组件之间的数据流动变得可预测、可维护,也为我们后续讨论 v-model 原生支持铺平了道路。