Vue3 升级踩坑:子组件 click 事件为什么会触发两次?

升级 Vue3 遇到的"灵异事件"

之前做老项目 Vue2 升 Vue3 的重构,本来以为就是改改 API、换换语法糖的体力活,结果在自测的时候撞见了一个挺邪门的问题。

有个封装好的基础按钮组件,父组件绑了个 @click 处理业务逻辑。在 Vue2 里跑得好好的,一到 Vue3 环境,点一下按钮,控制台硬生生打印出两条日志------事件触发了两次。

当时我第一反应是:难道事件冒泡没拦住?加了 .stop 修饰符,没用。检查父组件是不是绑了两次,也没有。最后排查了一圈,发现锅居然出在子组件的 emit 上。

今天就来盘一盘这个 Vue3 升级中极易踩坑的"双击"问题,顺便把底层的逻辑给大家捋清楚。

案发现场:到底是谁在偷偷发事件?

我们先来看看出问题的子组件代码,这是非常典型的 Vue2 遗留写法:

html 复制代码
<!-- 子组件 MyButton.vue -->
<template>
  <button class="my-btn" @click="handleClick">
    <slot />
  </button>
</template>

<script setup>
// 注意这里:没有声明 click 事件
const emit = defineEmits([]) 

const handleClick = () => {
  // 手动触发 click 事件
  emit('click') 
}
</script>

父组件就是很普通的监听:

html 复制代码
<MyButton @click="doSomething">点我</MyButton>

在 Vue2 中,如果你没有在 emits 选项里声明 click,父组件的 @click 会被当作原生事件,绑定到子组件的根元素(也就是那个 <button>)上。子组件内部 this.$emit('click') 触发的是自定义事件。两者井水不犯河水,通常我们靠手动 emit 来接管逻辑。

但在 Vue3 里,情况变了。Vue3 引入了更强大的 Fallthrough Attributes(透传 Attributes) 机制。

当子组件没有 显式声明 click 事件(也就是 defineEmits 里没写 'click')时,Vue3 会认为父组件传的 @click 是个原生 DOM 事件,于是直接把它透传绑定到子组件的根节点 <button> 上。

这时候,当你点击按钮,奇妙的事情发生了:

  1. 第一次触发 :你点击了 <button>,原生 click 事件触发,透传绑定的父组件 doSomething 执行。
  2. 第二次触发<button> 上的 @click="handleClick" 触发,执行了 emit('click')。因为父组件也监听了自定义的 clickdoSomething 又被执行了一次。

这就是为什么事件会"成精"触发两次。本质上,是原生事件透传手动自定义事件撞车了。

怎么破?两行代码搞定

搞清楚了原理,解决起来就非常简单了。这里给大家两个方案,按需取用。

方案一:删掉子组件的手动 emit(推荐)

既然 Vue3 已经贴心地帮你把原生事件透传到根节点了,对于这种纯包装类的 UI 组件,我们完全没必要再手动去 $emit 一次。

直接把子组件里手动触发事件的代码删掉,让它安安静静地做个透传容器:

html 复制代码
<template>
  <!-- 删掉 @click="handleClick",直接让原生 click 透传 -->
  <button class="my-btn">
    <slot />
  </button>
</template>

<script setup>
// 连 handleClick 和 emit 都可以省了,主打一个极简
</script>

这是最优雅、最符合 Vue3 直觉的做法。代码更少,逻辑更清晰,性能也稍微好那么一丢丢。

方案二:显式声明 emit(适用于需要拦截处理的场景)

如果你在子组件里确实需要做一些拦截逻辑(比如加个防抖、校验个权限,或者修改事件参数),不能直接透传,那就必须显式声明 click 事件。

html 复制代码
<template>
  <button class="my-btn" @click="handleClick">
    <slot />
  </button>
</template>

<script setup>
// 关键:在 defineEmits 中显式声明 'click'
const emit = defineEmits(['click']) 

const handleClick = (e) => {
  // 加点自定义逻辑
  console.log('拦截一下')
  emit('click', e) 
}
</script>

一旦你在 defineEmits 里声明了 click,Vue3 就会恍然大悟:"哦,原来这是个自定义事件,不是原生 DOM 事件。" 于是它不再 把父组件的 @click 透传到根节点,从而避免了两次触发的问题。

避坑指南:给 Vue3 升级人的几句掏心窝子话

说实话,从 Vue2 迁移到 Vue3,很多这种"暗坑"都是因为底层机制的微调引起的。借着这个 click 事件的问题,我给大家总结几条实操建议:

  • 养成声明 emits 的好习惯 :在 Vue3 中,强烈建议把所有子组件向外抛出的事件,都在 defineEmits 里明明白白地写出来。这不仅是为了防止透传冲突,更是为了组件的 TS 类型推导和代码可读性。
  • 警惕多根节点组件 :如果你的子组件有多个根节点(比如外面套了个 <div>,里面是个 <button>),Vue3 是无法自动透传 Attributes 的,这时候控制台会报 warning。你需要手动使用 v-bind="$attrs" 来指定透传目标。
  • 别太依赖"黑魔法" :Vue2 时代我们习惯了不声明 emits 也能随便 $emit,但在 Vue3 里,这种隐式行为往往是 bug 的温床。把组件的 Props 和 Emits 当作组件的 API 契约,严格定义,后期维护能少掉很多头发。

技术升级从来不是简单的 npm install,底层思维也得跟着迭代。希望这篇复盘能帮你在升级 Vue3 的路上少踩一个坑。

相关推荐
Nayana42 分钟前
《Web 到 HarmonyOS》-- Ability、Stage、模块化与路由导航
前端
ShineWinsu1 小时前
对于 Vue 3:从为什么学 Vue,到声明式渲染与数据响应式的解析
前端·javascript·vue.js
粥里有勺糖1 小时前
视野修炼-技术周刊第132期 | 一些有趣的组件
前端·github·aigc
এ慕ོ冬℘゜2 小时前
样式控制 .css ()
前端·css
leoZ2313 小时前
第 6 篇:SchemaForm 渲染器核心实现
前端·javascript·vue.js·人工智能·神经网络·机器学习·自然语言处理
CoderYanger3 小时前
前端基础——JavaScript(基础语法)(下篇)
java·开发语言·前端·javascript·程序人生·面试·职场和发展
还是大剑师兰特3 小时前
vue项目浏览器版本判别,低于IE11跳转到新页面
前端·javascript·vue.js
计算机魔术师3 小时前
奥尔特曼致歉 GPT-6 Astra 发布混乱,现已面向所有 Plus / Pro 等用户推出
前端
落樱弥城3 小时前
RHI 设计考量:不同 API 差异分析
服务器·前端·性能优化