Vue3+Vuetify3组件警告:update:modelValue未声明于emits选项
update:modelValue仍报错 我正在基于Vue3+Vuetify3开发项目,编写了一个侧边对话框组件,父组件通过v-model控制其显示状态,但控制台持续输出警告:
component emitted event "update:modelValue" but it is neither declared in the emits option nor as an "onUpdate:modelValue" prop.
我已经在组件的emits接口中声明了update:modelValue事件,却仍出现该警告,想知道问题出在哪里。
侧边对话框组件代码
<template> <div> <slot name="activator" :show="() => setShowDialog(true)" :close="() => setShowDialog(false)" /> <VDialog v-model="componentData.showDialog" @click:outside="onClickOutside" :width="componentProperties.width" :min-width="componentProperties.minWidth" :max-width="componentProperties.maxWidth" :content-class="contentClass" persistent transition="slide-x-reverse-transition" > <VCard v-show="componentData.showDialog" v-fill-parent> <VCardTitle v-if="hasTitle" :class="componentProperties.titleClass"> <VRow justify="center" align="center"> <slot name="title"> <span v-if="componentProperties.title">{{ componentProperties.title }}</span> </slot> <VSpacer /> <VCol v-if="componentProperties.showTopCloseButton" cols="1"> <VBtn @click.stop.prevent="() => setShowDialog(false)" color="transparent" elevation="0" size="medium" > <IconifyMdiClose /> </VBtn> </VCol> </VRow> </VCardTitle> <slot name="body"> <VCardText> <slot name="content-prepend" /> <slot> <slot name="content" /> </slot> <slot name="content-append" /> </VCardText> </slot> <VCardActions> <slot name="actions" :show="() => setShowDialog(true)" :close="() => setShowDialog(false)" /> </VCardActions> </VCard> </VDialog> </div> </template> <script setup lang="ts"> import { Events } from '@enums/events'; export interface IDialogConfirmComponentProperties { modelValue?: boolean; modelData?: any; title?: string; isActive?: boolean; showTopCloseButton?: boolean; titleClass?: string; fillHeight?: boolean; persistent?: boolean; minWidth?: string | number; maxWidth?: string | number; width?: string | number; } export interface IDialogConfirmComponentEvents { (e: 'onOutsideClicked'): void; (e: 'update:isActive', val?: boolean): void; (e: 'update:modelValue', visible?: boolean): void; } export interface IDialogConfirmComponentData { showDialog?: boolean; } const componentProperties = withDefaults(defineProps<IDialogConfirmComponentProperties>(), { persistent: false, fillHeight: true, modelValue: false, isActive: true, showTopCloseButton: false, titleClass: 'pt-5 text-wrap', width: 'auto', minWidth: 650, maxWidth: 650, }); const componentData = reactive<IDialogConfirmComponentData>({ showDialog: false, }); const emits = defineEmits<IDialogConfirmComponentEvents>(); const slots = useSlots(); const hasTitle = computed(() => !!slots['title'] || !!componentProperties?.title); const contentClass = computed(() => { return ['side-dialog', componentProperties.fillHeight ? 'fill-height' : ''].join(' '); }); function onClickOutside(payload: MouseEvent) { emits('onOutsideClicked'); if (componentProperties.persistent) return; setShowDialog(false); } watch( () => componentData.showDialog, showDialog => { componentData.showDialog = showDialog; emitsDialogVisibleChanges(); }, ); watch( () => componentProperties.modelValue, showDialog => { componentData.showDialog = showDialog; }, ); function setShowDialog(value: boolean) { componentData.showDialog = value; } function emitsDialogVisibleChanges() { if (componentProperties.modelValue === undefined || componentProperties.modelValue === null) return; emits('update:isActive', componentData.showDialog); emits('update:modelValue', componentData.showDialog); } </script> <style lang="css"> .side-dialog { inset-block-start: 0 !important; inset-inline-end: 0 !important; } </style>
父组件调用代码
<template> <SideDialog title="Configurations" show-top-close-button v-model="isDialogVisible"> <template #activator="{ show }"> <VBtn :variant="componentProperties.variant" @click.stop.prevent="show"> <VIcon>mdi-cog</VIcon> </VBtn> </template> <VRow justify="center" align="center"> <span>teste</span> </VRow> </SideDialog> </template> <script setup lang="ts"> import type { ButtonVariant } from '@/types/vuetify'; export interface IConfigurationComponentProperties { variant?: ButtonVariant; } const componentProperties = withDefaults(defineProps<IConfigurationComponentProperties>(), { variant: 'plain', }); const isDialogVisible = ref(false); </script>
问题原因分析
VDialog自带事件冒泡:你给
VDialog绑定了v-model="componentData.showDialog",而Vuetify的VDialog组件本身会在状态变化时(比如点击遮罩关闭对话框)触发update:modelValue事件。由于你没有监听并处理这个事件,它会向上冒泡,被Vue误认为是你的SideDialog组件发出的事件,但这个由VDialog触发的事件并未包含在你声明的emits列表中,因此触发警告。watch逻辑的无限循环风险:你在监听
componentData.showDialog的回调中,又执行了componentData.showDialog = showDialog,这会导致watch被反复触发,形成无限循环,同时也会触发不必要的事件emit。
解决方案
步骤1:处理VDialog的update:modelValue事件
将VDialog的v-model拆分为手动绑定modelValue和监听update:model-value,阻止事件冒泡并同步到组件内部状态:
<VDialog :model-value="componentData.showDialog" @update:model-value="handleDialogUpdate" @click:outside="onClickOutside" :width="componentProperties.width" :min-width="componentProperties.minWidth" :max-width="componentProperties.maxWidth" :content-class="contentClass" persistent transition="slide-x-reverse-transition" >
添加对应的处理函数:
function handleDialogUpdate(newValue: boolean) { componentData.showDialog = newValue; }
步骤2:修复watch的无限循环问题
移除watch回调中对componentData.showDialog的重复赋值:
watch( () => componentData.showDialog, showDialog => { emitsDialogVisibleChanges(); }, );
步骤3:优化事件emit逻辑(可选)
由于watch会监听内部状态变化并自动emit事件,可简化setShowDialog函数:
function setShowDialog(value: boolean) { componentData.showDialog = value; }
内容的提问来源于stack exchange,提问作者Vinicius Andrade

