很多前端开发者在做组件之间通信的时候,都会用到类似observers订阅、triggerEvent触发事件的模式——比如大家熟悉的Vue事件总线、甚至React的自定义事件,都是这类模式的变种。不过用起来爽的同时,藏着不少容易忽略的小陷阱,踩中了就会出现一些莫名其妙的异常,比如弹窗点一次关不掉、按钮点击没反应、页面越来越卡这些问题,今天就来唠唠这些隐藏的坑,再给大家说怎么避。
一、自定义组件通信的日常用法
这个模式其实特别好理解,你可以把事件总线当成一个“公共聊天群”:想要收消息的组件,就发个“我要进群,收到XX消息记得告诉我”的请求(对应observers订阅);想要发消息的组件,就往群里喊一声“我发了XX消息”(对应triggerEvent触发),这样不管是父子组件、还是没直接关联的兄弟组件,都能通过这个群传消息,不用一层一层麻烦地传参数。
1.1 基础用法示例
这里用大家最熟悉的Vue2技术栈来举例,代码都是日常开发里常写的:
// 技术栈:Vue2
import Vue from 'vue';
// 建一个全局的事件总线,相当于一个固定的公共聊天群
const eventBus = new Vue();
export default eventBus;
第一个子组件想收消息,就用订阅方法:
// 子组件A.vue(负责收消息)
import eventBus from './eventBus.js';
export default {
name: 'ComponentA',
created() { // 组件刚建好的时候
// 订阅名为updateData的消息,收到消息就执行对应的逻辑
eventBus.$on('updateData', (data) => {
console.log('我收到了数据:', data);
// 这里可以写组件要做的事,比如更新界面、调用接口
});
}
};
另一个子组件想发消息,就用触发方法:
// 子组件B.vue(负责发消息)
import eventBus from './eventBus.js';
export default {
name: 'ComponentB',
methods: {
sendMessage() { // 点按钮就触发消息
// 往群里喊一声:我发了updateData消息,带的数据是"我是B传的"
eventBus.$emit('updateData', '我是B发的测试数据');
}
}
};
二、那些容易踩的隐式陷阱
很多新手用这个模式,只会抄上面的代码,没注意到细节,等到出现异常才摸不着头脑。最常见的坑有两个,都是肉眼不容易发现的:
2.1 忘了解绑导致的重复执行
比如子组件A是一个弹窗,每次打开弹窗都会创建新的组件实例,每次实例都订阅了消息,但当用户关闭弹窗(组件被销毁)的时候,之前的订阅记录还留在聊天群里,没被删掉。等下次再打开弹窗,新的实例又订阅一次,这个群里就有了两条一模一样的订阅记录,再触发一次消息,就会执行两次逻辑——比如弹出两次提示、调用两次接口,明显不对,但又找不到哪里错了。
2.2 事件名写错导致的“假死”
这个坑更隐蔽,比如组件A订阅的是filterChange消息,组件B触发的是filterChanged消息,就差了一个字母,这时候群里的订阅者(组件A)根本听不到消息,组件B喊破喉咙也没人理,调试半天都以为是通信逻辑错了,其实只是手输错了事件名。
三、陷阱场景的详细示例
咱们拿一个实际的搜索页面举例:页面有三个组件,搜索框、筛选栏、结果列表,筛选栏负责选条件,选完后要通知结果列表更新内容,三个组件都用刚才的事件总线通信。
3.1 重复触发的具体场景(错误示例)
筛选栏组件每次打开都会订阅筛选消息,但没做解绑,就会出现重复执行:
// 错误的筛选栏组件,没解绑事件
import eventBus from './eventBus.js';
export default {
name: 'FilterBar',
created() { // 组件刚建好就订阅
// 订阅筛选变化的消息,收到就执行筛选逻辑
eventBus.$on('filterChange', (params) => {
console.log('筛选参数是:', params);
// 这里要调用接口拿结果,要是重复触发就会调用两次接口
});
}
};
当用户第一次打开筛选栏,订阅了一条消息;关掉筛选栏(组件销毁),但这条订阅还在群里;第二次打开筛选栏,又多了一条订阅。之后用户选条件,发送一次消息,群里就有两条相同的订阅,结果就是执行两次筛选逻辑,明显不对。
3.2 事件名写错的具体场景
刚才的例子里,筛选栏订阅的是filterChange,结果列表组件触发的是filterChanged,少了一个e,结果列表收不到消息,用户选完条件,结果页根本不更新,调试的时候会以为是哪里的逻辑错了,其实只是事件名打错了。
四、避坑指南
知道了陷阱的原因,避坑就很简单,记住三个关键点就行,都是日常开发就能做到的事。
4.1 组件销毁时一定要解绑事件
当组件快要消失(Vue里的beforeDestroy阶段),一定要把对应的订阅删掉,而且要注意,解绑的时候要和订阅时用同一个逻辑函数,不能用匿名函数,不然找不到对应的订阅记录,删不掉。
// 正确的筛选栏组件,带解绑逻辑
import eventBus from './eventBus.js';
export default {
name: 'FilterBar',
created() {
// 把要执行的逻辑存成变量,方便后面解绑用
this.filterLogic = (params) => {
console.log('筛选参数是:', params);
// 执行筛选逻辑
};
eventBus.$on('filterChange', this.filterLogic);
},
beforeDestroy() { // 组件快要消失时解绑
// 解绑对应事件,参数要和订阅时完全一样
eventBus.$off('filterChange', this.filterLogic);
}
};
4.2 事件名统一管理,避免手写错误
把所有事件名写在一个单独的常量文件里,不用每次手动打字,这样就不会写错,哪怕改事件名只要改一次就行,特别方便:
// 专门存事件名的文件eventNames.js,全是常量,不会写错
export const FILTER_CHANGE = 'filterChange';
export const UPDATE_DATA = 'updateData';
然后组件里引用这个常量,就不用担心打错了:
// 筛选栏组件正确用法,用常量代替手写
import eventBus from './eventBus.js';
import { FILTER_CHANGE } from './eventNames.js';
export default {
created() {
this.filterLogic = (params) => { /* 筛选逻辑 */ };
eventBus.$on(FILTER_CHANGE, this.filterLogic);
},
beforeDestroy() {
eventBus.$off(FILTER_CHANGE, this.filterLogic);
}
};
4.3 按需订阅,不要乱绑定
只在确实需要的组件里订阅事件,不要在公共组件、通用组件里随便加订阅,减少不必要的事件绑定,也能降低陷阱出现的概率。
五、总结
用observers订阅、triggerEvent触发的模式做组件通信,确实很方便,尤其是非父子组件之间传值,不用一层一层传参数,节省很多重复代码。但这个模式的隐式陷阱也不少,最常见的就是没解绑导致的重复执行,还有手误写错事件名导致的通信失效。只要记住两个核心原则:组件销毁时一定解绑对应事件,所有事件名统一管理,就能避开大多数坑,让你的组件通信代码更稳定,不会出现那些莫名其妙的异常问题。
评论
围绕“自定义组件通信的隐式陷阱:observers与triggerEvent在使用中的异常场景”参与讨论