# 发布订阅
做前端开发无论用什么框架, 可能都需要涉及到组件间的通信,除了使用属性传递,事件回调,依赖收集,注入,全局状态管理对象(Vuex,Redux)等各自框架自带的方式外,挂载一个全局的事件总线也是可以的,而且可能是更通用的做法,和框架无关。 这个全局的事件总线对象简单点可以就是一个对象,也可以是实现事件绑定、触发、移除的一个对象。
# 1、全局对象思路
这种相对简单, 可以用在暴露组件的一些私有属性,比如一个组件A有一个属性叫做startData,有很多组件都要根据组件A的开始日期做一些查询的工作。我们可以这么做
cacheObj.js
const CacheObj = {
getStartDate: () => null
};
export { CacheObj };
然后组件A可以覆写这个方法,以下仅为示例代码, 可以是Vue的某个data属性, 也可以React的某个state
componentA.js
import { CacheObj } from './CacheObj.js';
let startDate = '20231201';
CacheObj.getStartDate = function () {
return startDate;
};
其他的组件就可以直接使用了,比如
other.js
import { CacheObj } from './CacheObj.js';
const startDate = CacheObj.getStartDate();
fetch(url, { startDate });
WARNING
当然这个有一个问题, 就是覆写的那个组件必须先于其他的组件加载, 提前覆写好函数。除非这个返回值有一个默认值。
这种方式相对来说, 还是比较容易根据线索找到设定的逻辑的。 也可以用这种方式直接影响另外组件要触发的操作。
# 2、全局事件总线
先提供一个全局事件总线的案例,当然这个东西网上一大堆,我写的肯定不是最完美的。只能说实现基本的绑定和触发没有问题。
class EventEmitter {
constructor() {
this.map = {};
}
on(eventName, fn) {
if (!eventName || typeof eventName !== 'string') {
throw new Error('eventName expect a String');
return;
}
if (typeof fn !== 'function') {
throw new Error('callback expect a function');
return;
}
if (this.map[eventName] === undefined) {
this.map[eventName] = [];
}
this.map[eventName].push(fn);
}
off(eventName, fn) {
if (!eventName || typeof eventName !== 'string') {
throw new Error('eventName expect a String');
return;
}
const fns = this.map[eventName];
if (fns && fns.length > 0) {
if (!fn) {
this.map[eventName] = undefined;
delete this.map[eventName];
} else {
for (let i = fns.length - 1; i >= 0; i--) {
if (fns[i] === fn) {
fns.splice(i, 1);
// break;
}
}
}
if (fns.length === 0) {
this.map[eventName] = undefined;
delete this.map[eventName];
}
}
}
clear() {
this.map = {};
}
// 判断是否有订阅某一个事件
has(eventName) {
return this.map[eventName] && this.map[eventName].length > 0;
}
emit(eventName, ...args) {
if (!eventName || typeof eventName !== 'string') {
throw new Error('eventName expect a String');
return;
}
const fns = this.map[eventName];
if (fns && fns.length > 0) {
fns.forEach(fn => {
fn(...args);
});
} else {
// throw new Error(eventName + ' is not register');
}
}
}
const emitter = new EventEmitter();
export default emitter;
// 通知多次,直到有一次成功了。主要用在有时候事件还没绑定就开始通知 导致接收不到。
export function emitTimes(eventName, times = 5, ...args) {
let index = 1;
const notify = () => {
if (emitter.has(eventName)) {
emitter.emit(eventName, ...args);
return;
}
if (index >= times) {
return;
}
setTimeout(() => {
index++;
notify();
}, 1000);
};
return notify();
}
用法
import emitter from 'emitter.js';
function onChange(val) {
console.log(val);
}
// 绑定
emitter.on('on-change', onChange);
// 触发
emitter.emit('on-change', 'x');
// 注销
emitter.off('on-change', onChange);
// 判断是否有绑定on-change事件
emitter.has('on-change');
// 清空所有注册的事件
emitter.clear()
WARNING
这种方式在组件间通信也要注意事件绑定的先后问题。需要规避还没有绑定就触发事件, 导致事件丢失。
为了尽量避免上述的情况,提供了 emitTimes
函数。这个函数会先判断该有的事件是否有绑定, 如果没有, 过1秒再试,可以自定义重试的次数。能有效避免因为某些异步问题,组件加载顺序问题导致的通知不同步问题。
用法如下
import { emitTimes } from 'emitter.js';
emitTimes('on-change', 10, 'a'); // 尝试触发on-change事件,最多尝试10次,一共需要大概10秒