前端发布-订阅模式原理和实现

337 阅读2分钟

前端项目中组件之间通信方式有很多种,对于非层级组件之间的通信,一般采取订阅-发布模式,这篇文章整理实现思路和用法。

要素

要实现订阅-发布模式,需要考虑几个关键要素:

  1. 存储订阅事件名 和 订阅事件关系的map
  2. 订阅方法,用于订阅某个事件,入参必须有事件名和订阅事件
  3. 发布方法,用于发布某个事件,入参必须有事件名和发布的内容
  4. 取消订阅方法,用于组件取消对某个事件的订阅

实现

关系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', {});
 };