前端项目中组件之间通信方式有很多种,对于非层级组件之间的通信,一般采取订阅-发布模式,这篇文章整理实现思路和用法。
要素
要实现订阅-发布模式,需要考虑几个关键要素:
- 存储订阅事件名 和 订阅事件关系的map
- 订阅方法,用于订阅某个事件,入参必须有事件名和订阅事件
- 发布方法,用于发布某个事件,入参必须有事件名和发布的内容
- 取消订阅方法,用于组件取消对某个事件的订阅
实现
关系Map
使用 Map
类型实现储订阅事件名 和 订阅事件之间的关系,可能存在一个或多个组件订阅同一个事件,所以每个事件对应的值应该是一个数组,存储多个回调,每个回调都用独一无二的标识,可直接通过标识取消订阅。
export interface ITopicValue {
token: number; // 每个事件独一无二的标识
func(event?: any, topic?: string): any; // 事件对应的回调函数
}
topicSubsMap = new Map<string, ITopicValue[]>();
uuid = 0;
_getUUID() {
return ++this.uuid;
}
订阅事件
当组件需要订阅某个事件时,就说明需要在某个事件触发时,执行对应的操作。
- 入参,需要有事件名和订阅事件的回调函数
- 可支持一次订阅多个事件
- 事件被第一个组件订阅,需要初始化
/**
* 订阅事件
* @param topic string | array 订阅事件名
* @param func function(topic, event) 回调函数
* @returns {*|number}
*/
subscribe(topic: string | string[], func: (topic: string, event: any) => any) {
uuid = this._getUUID();
if (Array.isArray(topic)) {
// 订阅多个事件,依次执行订阅
topic.forEach(item => {
this.subscribe(item, func);
});
return uuid;
}
// 首次执行订阅,需要初始化topic对应的值为 []
if (!this.topicSubsMap.has(topic)) {
this.topicSubsMap.set(topic, []);
}
this.topicSubsMap.get(topic)!.push({
token: uuid,
func: func
});
return uuid;// 给订阅者返回token
}
发布事件
发布事件可以理解为触发事件,需要有事件名以及携带的数据,事件对应的全部订阅者,也就是全部回调都需要执行
/**
* 事件发布
* @param topic
* @param resultObj
*/
publish(topic: string, resultObj?: any) {
if (!this.topicSubsMap.has(topic)) {
return false;
}
// 取出所有的订阅者的回调
let subscribers = this.topicSubsMap.get(topic) || [];
subscribers.forEach((sub: object | any) => {
sub.func(resultObj);
});
return true;
}
取消订阅
一般当组件卸载时,为避免资源浪费,需要取消对事件的订阅,因为回调都存储了对应的token,所以直接通过token就可以取消事件的订阅。
unsubscribe(token: any) {
for (let subs of this.topicSubsMap.values()) {
for (let i = 0; i < subs.length; i++) {
if (subs[i].token == token) {
subs.splice(i--, 1);
}
}
}
return false;
}
完整代码如下
export interface ITopicValue {
token: number;
func(event?: any): any;
}
/**
* 统一消息管理, 将消息发送给所有订阅这个消息类型的模块
* 采用 订阅/发布(观察者) 这种设计模块式开发
*/
class MsgCenter {
topicSubsMap = new Map<string, ITopicValue[]>();
uuid = 0;
_getUUID() {
return ++this.uuid;
}
/**
* 事件发布
* @param topic
* @param resultObj
*/
publish(topic: string, resultObj?: any) {
if (!this.topicSubsMap.has(topic)) {
return false;
}
let subscribers = this.topicSubsMap.get(topic) || [];
subscribers.forEach((sub: object | any) => {
sub.func(resultObj);
});
return true;
}
/**
* 订阅事件
* @param topic string | array
* @param func function(topic, event)
* @param uuid
* @returns {*|number}
*/
subscribe(topic: string | string[], func: (topic: string, event: any) => any) {
uuid = this._getUUID();
if (Array.isArray(topic)) {
topic.forEach(item => {
this.subscribe(item, func, uuid);
});
return uuid;
}
if (!this.topicSubsMap.has(topic)) {
this.topicSubsMap.set(topic, []);
}
this.topicSubsMap.get(topic)!.push({
token: uuid,
func: func
});
return uuid;
}
unsubscribe(token: any) {
for (let subs of this.topicSubsMap.values()) {
for (let i = 0; i < subs.length; i++) {
if (subs[i].token == token) {
subs.splice(i--, 1);
}
}
}
return false;
}
reset() {
this.topicSubsMap.clear();
}
}
export default MsgCenter;
使用
MsgCenter初始化
要保证全局的发布-订阅通信,所以需要全局实例化一个类
const msgCenter = new MsgCenter();
export default msgCenter;
订阅和取消订阅
一般在组件挂载时订阅事件,组件将要卸载时,取消订阅事件
import React, { Component } from 'react';
import msgCenter from 'utils';
class SubscribeComponent extends Component {
componentDidMount() {
this.msgToken = msgCenter.subscribe('topic-name', (data) => {
// 执行回调逻辑
console.log(data)
});
}
componentWillUnmount() {
// 取消订阅
msgCenter.unsubscribe(this.msgToken);
}
render() {
// ……
}
}
发布
在任何一个方法中都可以发布事件
const publish = () => {
// 逻辑
msgCenter.publish('topic-name', {});
};