初步定位
最近在项目里看到下面这个 Warning,顺着调用链排查后发现源头在一个内部 hook。
Component emitted event "register" but it is neither declared in the emits option nor as an "onRegister" prop.
最初以为是简单的 instance.emit('register') 调用时事件未注册导致的,本着对控制台 Warning ‘零容忍’的态度,我在 hook 内部加了一个是否声明该 event 的检查。
function createInstanceEmitChecker(instance: ComponentInternalInstance) {
const emits = instance.type?.emits
return (event: string) => (Array.isArray(emits) ? emits.includes(event) : !!emits?.[event])
}
function emitWithCheck(instance: ComponentInternalInstance, event: string, ...args: any[]) {
const hasRegisterEmit = createInstanceEmitChecker(instance)
if (!hasRegisterEmit(event))
return
instance.emit(event, ...args)
}
在组件内部显式声明 defineEmits(['register']) 后,控制台的 Warning 立刻消失了。
事情还远远没有结束,在排查过程中,我发现原本正常调用 hook 的页面,经过上面的代码调整后,register 的事件回调失效了。排查代码后发现这些原本正常的组件,是没有注册 register 事件的,但是控制台没有显示 Warning,这引起了我的思考:
- 是否存在某种隐式的事件注册机制?
- 该 Warning 的触发逻辑,并非单纯的未注册事件
源码溯源
为了证实猜想,我先在 Vue SFC Playground 编写了一个最小复现案例:未声明 emits 时的调用
出乎意料的是,控制台并没有弹出任何警告,那么,最初那个扎眼的 Warning 到底是如何触发的?
带着这种好奇心,深入翻阅 Vue 源码,最终在 componentEmits.ts 中找到了该 Warning 的判断逻辑,我将其核心逻辑简化如下:
if (emitsOptions) {
// 检查事件是否在 emits 选项中定义
if (!event in emitsOptions) {
// 如果未定义,进一步检查是否作为 Props (如 onRegister) 传入
const handlerKey = toHandlerKey(camelize(event))
if (!propsOptions || !(handlerKey in propsOptions)) {
warn(
`Component emitted event "${event}" but it is neither declared in `
+ `the emits option nor as an "${handlerKey}" prop.`
)
}
}
}
根据源码逻辑,我重新构建了一个能稳定触发警告的案例:声明 emits 后触发的合法性警告
总结
Vue 的 emit 校验机制遵循按需启用原则,只有显式的用 defineEmits 声明事件,才会触发事件的合法性检查,如果一个组件完全没有定义 emits,Vue 会保持静默,但只要你定义了其中任何一个事件,Vue 就会认为你开启了严格模式,此时再调用任何未注册的事件,这行扎眼的 Warning 就会如约而至**