二次元绘画创作
56.21M · 2026-02-04
Vue3 中,跨组件通信是高频刚需——父子组件用 props/emits 足够,但祖孙组件、兄弟组件、任意层级跨组件通信时,选择哪种方案就成了开发者的难题。
mitt、tiny-emitter 是两大主流事件总线库,provide+inject 是 Vue 内置API,搭配 Symbol 可解决命名冲突,这4种是最常用的方案。但它们的体积、易用性、性能、适用场景天差地别,选对能少写冗余代码,选错则会埋下维护隐患,这也是本文的核心价值所在。
在深入对比前,先明确每种方案的核心定位,避免混淆,快速匹配自身需求:
以下是4种方案的核心维度对比,结合实操体验和项目场景,每一项都对应实际开发中的痛点,建议收藏备用。
| 方案 | 体积(min+gzip) | 是否需额外安装 | Vue3 兼容性 | 核心优势 |
|---|---|---|---|---|
| mitt | ≈200B | 是(npm i mitt) | 完美兼容(官方推荐) | 轻量、API简洁、支持批量解绑 |
| tiny-emitter | ≈150B | 是(npm i tiny-emitter) | 完美兼容 | 极致小巧、API贴近原生EventBus |
| provide+inject | 0体积(内置) | 否 | 完美兼容 | 无需额外依赖、原生支持、状态共享便捷 |
| provide+inject+Symbol | 0体积(内置+原生Symbol) | 否 | 完美兼容 | 解决命名冲突、大型项目友好、无额外开销 |
实操是核心,以下是每种方案的最简实现代码,重点看“通信流程”和“API复杂度”。
步骤:创建总线实例 → 发送事件 → 事件 → 解绑事件(避免内存泄漏)
// 1. 创建总线实例(utils/bus.js)
import mitt from 'mitt'
export const bus = mitt()
// 2. 发送事件(组件A)
import { bus } from '@/utils/bus'
bus.emit('sendMsg', 'Hello Vue3')
// 3. 事件(组件B,任意层级)
import { bus } from '@/utils/bus'
bus.on('sendMsg', (msg) => {
console.log('接收消息:', msg) // 输出:Hello Vue3
})
// 4. 解绑事件(组件销毁时,必写)
onUnmounted(() => {
bus.off('sendMsg') // 解绑单个事件
// bus.all.clear() // 解绑所有事件
})
API 与 mitt 类似,更贴近传统 EventBus,支持链式调用
// 1. 创建总线实例(utils/bus.js)
import Emitter from 'tiny-emitter'
export const bus = new Emitter()
// 2. 发送事件(组件A)
bus.emit('sendMsg', 'Hello tiny-emitter')
// 3. 事件(组件B)
bus.on('sendMsg', (msg) => {
console.log('接收消息:', msg)
})
// 4. 解绑事件(组件销毁时)
onUnmounted(() => {
bus.off('sendMsg')
})
步骤:祖先组件 provide 提供数据 → 后代组件 inject 注入数据(支持多层传递)
// 1. 祖先组件(提供数据)
import { provide } from 'vue'
export default {
setup() {
// 提供普通数据
provide('msg', 'Hello provide+inject')
// 提供方法(实现双向通信)
provide('changeMsg', (newMsg) => {
console.log('新消息:', newMsg)
})
}
}
// 2. 后代组件(注入数据,任意层级)
import { inject } from 'vue'
export default {
setup() {
const msg = inject('msg')
const changeMsg = inject('changeMsg')
console.log(msg) // 输出:Hello provide+inject
changeMsg('新的消息内容') // 调用祖先组件方法
}
}
核心:用 Symbol 创建唯一key,解决“不同组件注入同名key导致冲突”的问题
// 1. 创建唯一Symbol(utils/keys.js)
export const msgKey = Symbol('msg')
export const changeMsgKey = Symbol('changeMsg')
// 2. 祖先组件(provide)
import { provide } from 'vue'
import { msgKey, changeMsgKey } from '@/utils/keys'
setup() {
provide(msgKey, 'Hello Symbol')
provide(changeMsgKey, (newMsg) => {
console.log(newMsg)
})
}
// 3. 后代组件(inject)
import { inject } from 'vue'
import { msgKey, changeMsgKey } from '@/utils/keys'
setup() {
const msg = inject(msgKey)
const changeMsg = inject(changeMsgKey)
// 无需担心命名冲突
}
易用性排序:tiny-emitter ≈ mitt > provide+inject+Symbol > provide+inject(Symbol需额外维护key,略繁琐)
性能排序:provide+inject ≈ provide+inject+Symbol > mitt ≈ tiny-emitter(内置API无额外开销,事件总线有轻微/解绑开销)
适用场景差异:
坑点1:组件销毁时未解绑事件,导致多次渲染后重复触发(内存泄漏); 解决方案:在 onUnmounted 钩子中,手动解绑当前组件的所有事件。
坑点2:事件名拼写错误,导致通信失败(无报错提示); 解决方案:用常量统一管理事件名,避免手写字符串(如 export const EVENT_MSG = 'sendMsg')。
坑点1:inject 注入的属性为 undefined,未找到对应 provide; 解决方案:确认祖先组件已 provide,且注入的 key 完全一致(Symbol 必须是同一个实例,不能重复创建)。
坑点2:provide 提供的是普通值,后代组件修改后不响应; 解决方案:用 ref/reactive 包装提供的值,实现响应式通信(如 provide('msg', ref('Hello')))。
其实这4种方案没有绝对的优劣,核心是“匹配项目场景”——无需盲目追求“最先进”,能解决问题、降低维护成本,就是最好的选择。掌握它们的差异和踩坑点,Vue3 跨组件通信就能游刃有余~