vue3项目中在一个组件中点击了该组件中的一个按钮,那么如何去触发另一个组件中的事件?
先抓住本质
两个独立组件之间互相“触发事件”,本质是:把它们的交互提升到一个共同的中介(父组件或全局总线/状态),由中介来转发。不要直接跨组件互调,这会打乱数据流和可维护性。
常见且推荐的三种方式
1) 兄弟组件通过父组件中转(最标准)
- 适用:两个组件有共同父级。
- 思路:
- 子组件 A 点击按钮,用
emit('xxx')
通知父组件。 - 父组件接到后,改变一个响应式状态或直接调用子组件 B 的公开方法。
- 子组件 B 通过 props 或
defineExpose
的方法感知变化并执行事件。
- 子组件 A 点击按钮,用
示例:
子组件 A(发起方)
<!-- A.vue -->
<template><el-button @click="$emit('request-do-something')">点我触发B</el-button>
</template>
<script setup>
defineEmits(['request-do-something'])
</script>
子组件 B(接收方,提供动作)
<!-- B.vue -->
<template><div>我是B组件</div>
</template>
<script setup>
import { ref } from 'vue'// 暴露一个可被父组件调用的方法
const doSomething = () => {console.log('B 接收到动作,执行自己的逻辑')
}
defineExpose({ doSomething })
</script>
父组件(中介)
<!-- Parent.vue -->
<template><A @request-do-something="handleFromA" /><B ref="bRef" />
</template>
<script setup>
import { ref } from 'vue'
import A from './A.vue'
import B from './B.vue'const bRef = ref(null)const handleFromA = () => {// 方式1:直接调 B 的公开方法bRef.value?.doSomething()// 方式2:若 B 通过 props 驱动,则改状态传给 B
}
</script>
优点:数据流清晰、类型可控、最符合 Vue 思想。
缺点:需要父组件参与。
2) 使用事件总线(小型项目可用,注意约束)
- 适用:层级较深或没有方便的共同父组件,但规模不大。
- 思路:用一个简单的事件发布/订阅实例(例如 tiny-emitter、mitt),A 发布事件,B 订阅事件。
事件总线 bus.js
// bus.ts
import mitt from 'mitt'
export type Events = {'trigger-b': void
}
export const bus = mitt<Events>()
A.vue
<script setup lang="ts">
import { bus } from '@/bus'
const onClick = () => bus.emit('trigger-b')
</script><template><el-button @click="onClick">点我触发B</el-button>
</template>
B.vue
<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue'
import { bus } from '@/bus'const doSomething = () => {console.log('B 收到来自 A 的事件')
}onMounted(() => {bus.on('trigger-b', doSomething)
})
onBeforeUnmount(() => {bus.off('trigger-b', doSomething)
})
</script><template><div>我是B组件,监听了事件总线</div>
</template>
优点:解耦层级。
缺点:事件到处飞,调试/维护成本上升。务必集中定义事件名、在卸载时取消订阅。
3) 使用全局状态(Pinia/Redux风格)
- 适用:事件背后拥有可持久的“状态”或“意图”,并且有多个组件会读写它。
- 思路:A 写入 store 中的一个状态(或调用 action),B 通过订阅/计算属性响应这个状态变化并执行逻辑。
定义 store
// stores/useActionStore.ts
import { defineStore } from 'pinia'
export const useActionStore = defineStore('action', {state: () => ({tick: 0}),actions: {triggerB() {this.tick++ // 改变一个值作为触发信号}}
})
A.vue
<script setup lang="ts">
import { useActionStore } from '@/stores/useActionStore'
const store = useActionStore()
const onClick = () => store.triggerB()
</script><template><el-button @click="onClick">点我触发B</el-button>
</template>
B.vue
<script setup lang="ts">
import { watch } from 'vue'
import { useActionStore } from '@/stores/useActionStore'const store = useActionStore()
const doSomething = () => {console.log('B 响应 store 的 trigger')
}watch(() => store.tick,() => doSomething()
)
</script><template><div>我是B组件,响应 store 的变化</div>
</template>
优点:可观测、易测试、从事件升级为状态驱动。
缺点:引入全局依赖,不适合一次性的小交互。
选择建议(给你一个决策树)
- 两组件有共同父级,并且触发是局部的:用“父组件中转 + defineExpose”(方式1)。
- 跨层级且项目较小:用事件总线 mitt(方式2),记得集中管理事件名与解绑。
- 触发动作在业务上更像“状态变化”,且会被多处消费:用 Pinia(方式3)。
关键细节与常见坑
- 不要让子组件直接 import 另一个子组件并调用内部方法,这会造成强耦合和循环依赖。
- 如果用 ref 调子组件方法,确保使用
defineExpose
暴露;否则在<script setup>
下方法拿不到。 - 事件名请统一常量化,避免魔法字符串。TypeScript 项目优先为事件做类型标注。
- 在事件总线方案中,一定在
onBeforeUnmount
里off
,避免内存泄漏与重复触发。 - 在 store 方案中,不要把纯“瞬时事件”长期化;可以用“自增 tick”或“时间戳”作为一次性信号。
需要我基于你的现有代码结构(比如你已经在用 Pinia 或者已经有父级)给出最小改造的落地版本吗?发我两个组件的简要代码,我帮你嵌进去。