先记住这个答案
Vue 组件事件推荐在 JavaScript 中声明和触发 camelCase 名称,例如 submitForm;父模板用 @submit-form 监听。Vue 会完成这两种形式的匹配,这与 prop 的 camelCase/kebab-case 对应规则一致。若在渲染函数或 JSX 中监听,应按对应工具的属性约定使用 onSubmitForm,而不是照搬模板字符串。事件不会像原生 DOM 事件那样向祖先冒泡,只能由直接父组件监听后决定是否继续转发。团队应为每个事件固定一个语义名称,避免同时 emit 多种大小写别名。
- 脚本声明与 emit 使用 camelCase
- 模板监听推荐 kebab-case
- 组件事件只由直接父级接收
模板属性习惯决定推荐形式
HTML 属性名在一些解析环境中会被转为小写,kebab-case 不依赖大小写边界,更适合作为模板中的可见形式。单文件组件编译器能处理 camelCase,但统一模板风格可以减少内联 DOM 模板与预编译模板之间的差异。
脚本侧仍使用 camelCase,便于 TypeScript 类型键、对象成员和自动补全。把事件命名为 onSubmit 或 handleSubmit 会混淆“已经发生的事实”与“父级处理函数”,更清楚的事件名通常描述动作或状态变化。
声明一次并沿契约传递载荷
下例子组件声明 submitForm,在按钮点击时发送表单编号。父级模板用 @submit-form 接收。声明 emits 还能把已知组件事件从透传属性中分离,并为类型检查或运行时校验提供入口。
如果使用 render 函数,父级传入 onSubmitForm 监听属性;这是 VNode 属性约定。不要据此把模板也写成混杂的大小写风格,团队应在各语法层遵循对应约定。
import { defineComponent, h } from 'vue'
const SubmitButton = defineComponent({
emits: ['submitForm'],
setup(_props, { emit }) {
return () => h('button', {
onClick: () => emit('submitForm', 7)
}, '提交')
}
})
const submittedIds: number[] = []
const Parent = defineComponent({
setup() {
return () => h(SubmitButton, {
onSubmitForm: (id: number) => submittedIds.push(id)
})
}
})render 函数中的 onSubmitForm 对应模板 @submit-form。点击子按钮会把 7 交给直接父级监听器;submittedIds 只是演示接收位置,业务中应进入响应式状态或动作。
跨层通信要显式选择机制
孙组件 emit 的事件不会自动穿过父组件到达祖先。父组件可以重新声明并转发,或把跨层依赖改为 provide/inject、状态仓库或组合式服务。事件总线也需要清理、命名和类型策略,不能因为“事件”二字就假设行为相同。
原生 DOM 事件的大小写、冒泡和修饰符规则是另一套契约。排查监听未触发时,先确认发出的是组件事件还是落到根节点的原生监听,再核对父子是否直接相连及事件名是否一致。
容易答错的地方
- 同时发出 camelCase 和 kebab-case 两个事件
- 这会让一次业务动作触发两套监听,增加重复处理风险。固定脚本契约,依赖 Vue 在模板侧完成名称对应即可。
- 认为事件会自动冒泡到任意祖先
- 组件事件没有 DOM 冒泡链,也不会自动被爷爷组件监听。需要父级显式监听后重新转发,或改用适合跨层共享的通信机制。
面试官还会怎么问?
事件名可以带冒号吗?
可以,v-model 就使用 update:modelValue 这类命名。自定义命名空间应有稳定含义,并在 emits 类型中完整声明,不能依赖大小写规则修正冒号两侧的拼写错误。
为什么建议声明 emits?
声明能记录公开契约、支持类型或运行时校验,并避免同名监听被当作普通透传属性落到根节点。它不会让事件自动冒泡。
DOM 内联模板还要注意什么?
浏览器会先解析 HTML 并归一化属性名,因此模板事件和 prop 更应使用 kebab-case。构建期编译的 SFC 限制较少,但保持一致能减少迁移差异。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。