05|事件监听器与属性继承
第二章:组件模型与通信纵览 / 05。
@click写在组件上时,既可能是组件事件监听器,也可能作为 attrs 继续落到根 DOM;分界线就是组件是否声明了该事件。
从一个可运行按钮开始
<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><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。
父级传入的键怎样分类
| 父级写法 | 子组件是否声明 | 进入哪里 | 最终行为 |
|---|---|---|---|
@click | defineEmits 声明 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,但保留:
<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 监听器。