Vue 传参踩坑之旅——事件总线与 props
缘由
今天突然发现项目出现了一个 bug,这里简单描述一下。
这里有 A、B、C、D 四个组件,关系为 A - 祖先、B - 父、C - 子、D - 叔(实际业务组件关系复杂很多)。
- A - 祖先
- B - 父
- C - 子
- D - 叔
- B - 父
现在 C 组件要用到 D 组件中的一个方法,由于组件关系比较复杂,我为了省事直接使用 mitt 进行自定义事件的触发,但是 D 组件又可能有多个(通过 v-for
生成的),此时就出现了问题,当存在多个 D 组件时,在 C 组件中调用了 D 中的方法会同时触发所有 D 组件的该事件。
有的小伙伴看完就知道问题所在了,没明白的也没关系,下面我会结合代码进行详细说明。
代码详细说明
先说明一下目录结构:
- App.vue
- A.vue
- B.vue
- C.vue
- D.vue
- mitt.js
<!-- App.vue -->
<template>
<A v-for="item of 2" :key="item"></A>
</template>
<script setup>
import A from "./A.vue";
</script>
<!-- A.vue -->
<template>
<B></B>
<D></D>
</template>
<script setup>
import B from "./B.vue";
import D from "./D.vue";
</script>
<!-- B.vue -->
<template>
<C></C>
</template>
<script setup>
import C from "./C.vue";
</script>
<!-- C.vue -->
<template>
<button @click="handleDFunction">点击触发D组件方法</button>
</template>
<script setup>
import mitt from "./mitt";
function handleDFunction() {
mitt.emit("handled");
}
</script>
<!-- D.vue -->
<template></template>
<script setup>
import mitt from "./mitt";
function handled() {
console.log("D组件方法被触发");
}
mitt.on("handled", handled);
</script>
import mitt from 'mitt'
export default new mitt()
以上就是精简后的示例代码,可以简单理解为兄弟组件传参。
效果展示
此时页面会显示两个 <button>
按钮。
当随便点击了一个后会输出两次 “D组件方法被触发”。
我们想要的效果是只会触发当前兄弟组件的事件,而此时触发了所有 D 组件的该事件。
问题分析
这个 bug 的原因其实也很简单,这是由于 mitt.on
注册同名事件会在数组后面追加,而非覆盖或者其他思路,简单通过代码说明一下它的源码实现:
function mitt() {
const eventMap = new Map(); // 事件表
return {
// 事件注册
on(key, callback) {
// 获取当前名称的事件列表
const curEventList = eventMap.get(key)
// 不是第一次注册该名称的事件,追加到数组最后
if (curEventList) {
curEventList.push(callback)
}
// 第一次注册该名称的事件,事件表中添加该事件列表
else {
eventMap.set(key, [callback])
}
},
// 事件派发
emit(key, ...args) {
// 获取当前名称的事件列表
const curEventList = eventList.get(key)
// 存在该事件,则遍历数组依次执行
if (curEventList) {
curEventList.forEach((curEvent) => {
curEvent(args)
})
}
},
}
}
现在就能知道为什么会出现这种问题了,因为每一个 D 组件都对该事件名称进行了注册,因此实际上该事件名称注册了 n 个该事件,所以当 emit 派发时就会依次执行。
解决的化只需要将 emit 替换为 props / emits
就解决了。
总结
这个 bug 其实给了我蛮多启发的,也能理解之前看到别人说业务组件中尽量使用 props / emits
了,事件总线的原理我其实是知道的,但是还是会产生这种 bug,因为随着业务的发展,新功能的产生以及老功能的优化等都可能对原来的数据结构或组件结构进行更改,在我们写业务代码时是比较难精准的预料到未来的变化的,此时就需要我们的代码有足够高的容错性,诸如 provide / inject
、事件总线等组件通信方式固然很方便,但是在代码重构或其他一些比较重大改动时项目的数据流又会变得难以预测。
props / emits
的缺点:
- 代码量会变得更多、冗余
props / emits
的优点:
- 组件数据流变得清晰
- 当项目结构发生变化时重构会更简单
- 不容易产生奇怪的问题