Skip to content

05|事件监听器与属性继承 ​

第二章:组件模型与通信纵览 / 05。@click 写在组件上时,既可能是组件事件监听器,也可能作为 attrs 继续落到根 DOM;分界线就是组件是否声明了该事件。

从一个可运行按钮开始 ​

vue
<script setup lang="ts">
defineOptions({ inheritAttrs: false })

const emit = defineEmits<{
  click: [event: MouseEvent]
}>()
</script>

<template>
  <button
    type="button"
    v-bind="$attrs"
    @click="emit('click', $event)"
  >
    <slot />
  </button>
</template>
vue
<script setup lang="ts">
import { ref } from 'vue'
import ActionButton from './ActionButton.vue'

const clickCount = ref(0)
const focused = ref(false)
</script>

<template>
  <ActionButton
    id="save-button"
    class="primary"
    @click="clickCount++"
    @focus="focused = true"
  >
    保存
  </ActionButton>
  <p>click={{ clickCount }}, focused={{ focused }}</p>
</template>

点击一次,clickCount 只增加一次;Tab 聚焦按钮时,focused 变成 true。

父级传入的键怎样分类 ​

父级写法子组件是否声明进入哪里最终行为
@clickdefineEmits 声明 click组件事件监听器只在 emit('click') 时调用
@focus未声明 focus$attrs.onFocus经 v-bind="$attrs" 绑定为原生监听器
id、class未声明 prop$attrs绑定到内部 <button>
默认插槽不是 attr$slots.default渲染在 <slot /> 位置

声明 emits 不只是提供类型,它还会把匹配的 onXxx 监听器从 fallthrough attrs 中移除。这样组件可以明确决定何时发出事件。

双触发是怎样产生的 ​

从 ActionButton.vue 删除 defineEmits,但保留:

vue
<button v-bind="$attrs" @click="$emit('click', $event)">
  <slot />
</button>

父级的 @click 因为没有匹配的 emits 声明,会作为原生监听器透传到 <button>;同一次原生点击先调用这份监听器。组件自己的处理器随后又 $emit('click'),父级监听器再被作为组件监听调用,于是一次操作可能得到两次回调。

修复不是加防抖,而是声明公开事件,或者根本不重复发出同名事件。协议错误应在协议层修复。

何时直接透传,何时重新发出 ​

  • 组件只是透明地包一层原生按钮,而且不需要改写点击语义:可以不声明 click,让监听器直接透传。
  • 组件要做权限判断、确认、埋点或载荷转换:声明事件,处理后只发出一次。
  • 行为不是原生点击语义:使用 confirm、remove-request 等领域事件,避免让调用方误以为拿到的是原始 DOM 事件。

无论哪一种,都要把监听器绑定到真正可交互的原生元素。把 @keydown.enter 和 @click 同时手动转成同一动作也要小心:原生 <button> 本就会把键盘激活转换成点击,重复绑定可能再次触发。

组件事件与 DOM 冒泡仍然不同 ​

ActionButton 发出的组件 click 只给直接父级组件。它的名字碰巧与 DOM click 相同,也不会因此沿 Vue 组件树冒泡。真正的 DOM 事件是否冒泡,取决于原生节点和事件修饰符;组件 emits 是否到达祖先,取决于中间组件有没有显式转发。

练习:不会重复触发的图标按钮 ​

实现 IconButton.vue 与父级页面:

  • prop:label: string,用于可见文本或 aria-label 的组件逻辑。
  • emit:声明 click,载荷保持为原始 MouseEvent。
  • attrs:id、class、disabled、data-testid 与未声明的 @focus 只绑定到内部原生 <button>。
  • 点击一次时父级计数必须只加 1;按钮为 disabled 时计数不变。
  • 用 Tab 聚焦时父级的 focus 记录增加一次,不额外手写 Enter 键处理器。
  • 临时删掉 emits 声明,复现并解释双触发,再恢复正确实现。

参考答案:查看第 2 章第 05 题参考答案。

通关自检:看到组件上的 @事件 时,你能仅凭 props/emits 声明和根节点绑定,推导它是组件监听器还是原生 fallthrough 监听器。

用心记录每一步成长