动手来封装一个JS的发布订阅组件

1,184 阅读5分钟

提及发布订阅,我们都知道是一种比较经典的设计模式。比如像redux等比较流行的一些库或者一些前端框架底层都会用来作为通讯机制,那么我们今天就来封装一个基于发布订阅的组件。

设计一个发布订阅的类库jpslib

jpslib这个名字代表了简单的发布订阅模式的类库,是实现我们目的工具。
先打造好工具,然后我们打算使用这个工具从’猿’进化为’员’。

API设计及实现

  • 首先,我们需要抽象一个订阅者,just show me the code:

    /*** base class of subscriber*/jps.ISubscriber = function () {};/*** must be implemented* interface to observe subscriber's interested notification*/jps.ISubscriber.prototype.listNotificationInterested = function () {    return [];};/*** must be implemented* interface to execute a notification interested by the subscriber*/jps.ISubscriber.prototype.executeNotification = function (notification) {    };

订阅者的职责第一便是列出他感兴趣的主题listNotificationInterested,第二是对于收到订阅的主题消息的处理executeNotification

  • 其次,我们抽象一个消息模型,show me the code:

    /*** base class of notification*/ jps.INotification = function (name, body, type) {    if(name === null || name === undefined) {        throw new Error('notification must have a name');    }    if(typeof name !== 'string') {        throw new TypeError('notification name must be string type');    }    this._name = name;    this._body = body;    this._type = type;};/*** interface to get notification's name*/jps.INotification.prototype.getName = function () {    return this._name;};/*** interface to get notification's body*/jps.INotification.prototype.getBody = function () {    return this._body;};/*** interface to get notification's type*/jps.INotification.prototype.getType = function () {    return this._type;};

    消息对象需要有他的具体名字、消息体、消息类型,其实一般我们只需要只知道消息名便可,消息类型只是用来以后做扩展使用。

  • 最后,是一个管理者的实现

    /**  * interface to subscribe notification  */  jps.subscribe =  function (subscriber) {      if(subscriber.listNotificationInterested && typeof subscriber.listNotificationInterested === 'function' && subscriber.executeNotification && typeof subscriber.executeNotification === 'function') {          var names = subscriber.listNotificationInterested();          if(names instanceof Array) {              //check names type              names.forEach(function (name) {                  if(typeof name === 'string') {                      //do nothing                  } else {                      throw new Error('interested notification name must be String type');                  }              });              //clear              jps._removeSubscribersFromMap(subscriber);              //add              names.forEach(function (name) {                  jps._addSubscriberToMap(name, subscriber);              });          } else {              throw new Error('interface listNotificationInterested of subscriber must return Array type');          }      } else {          throw new Error('subscriber must implement ISubscriber');      }  };  /**  * interface to publish notification  */  jps.publish = function (notification) {      if(notification.getName && typeof notification.getName === 'function') {          var subs = jps._getSubscribersFromMap(notification.getName()).concat();          subs.forEach(function (ele) {              ele.executeNotification(notification);          });      } else {          throw new Error('notification must implement INotification');      }  };  /**  * interface to create new notification object  */  jps.createNotification = function (name, data, type) {      if(typeof name === 'string') {          var Notification = function (name, data, type) {              jps.INotification.call(this, name, data, type);          };          jps._utils.extendClass(Notification, jps.INotification);          return new Notification(name, data, type);      } else {          throw new Error('notification name must be String type');      }  };  /**  * interface to unsubscribe notification interested by subscriber  */  jps.unsubscribe = function (notificationname, subscriber) {      if(subscriber.listNotificationInterested && typeof subscriber.listNotificationInterested === 'function') {          if(typeof notificationname === 'string') {              jps._removeSubscriberFromMap(notificationname, subscriber);          } else {              throw new Error('interested notification name must be String type');          }      } else {          throw new Error('subscriber must implement ISubscriber');      }  };  /**  *  interface to unsubscribe all the notification interested by subscriber  */  jps.unsubscribeAll = function (subscriber) {      if(subscriber.listNotificationInterested && typeof subscriber.listNotificationInterested === 'function') {          if(typeof notificationname === 'string') {              jps._removeSubscribersFromMap(subscriber);          } else {              throw new Error('interested notification name must be String type');          }      } else {          throw new Error('subscriber must implement ISubscriber');      }  };  /**  * the map from notification name to subscriber  */  jps._notificationMap = {};  /**  * get the subscribers from map by the notification name  */  jps._getSubscribersFromMap = function (notificationname) {      if(jps._notificationMap[notificationname] === undefined) {          return [];      } else {          return jps._notificationMap[notificationname];      }   };  /**  * get the interested names from map by the subscriber  */  jps._getInterestedNamesFromMap = function (subscriber) {      var retArr = [];      var arr;      for(var name in jps._notificationMap) {          arr = jps._notificationMap[name].filter(function (ele) {              return ele === subscriber;          });          if(arr.length > 0) {              retArr.push(name);          }      }      return retArr;  };  /**  * check the name is subscribed by the subscriber from map  */  jps._hasSubscriberFromMap = function (name, subscriber) {      var names = jps._getInterestedNamesFromMap(subscriber);      var retArr = names.filter(function (ele) {          return ele === name;      });      return retArr.length > 0;  };  /**  *  add the subscriber to the map  */  jps._addSubscriberToMap = function (name, subscriber) {      if(jps._hasSubscriberFromMap(name, subscriber)) {          //do nothing      } else {          var subs = jps._getSubscribersFromMap(name);          if(subs.length === 0) {              subs = jps._notificationMap[name] = [];          }          subs.push(subscriber);      }      return true;  };  /**  * remove the subscriber from the map  */  jps._removeSubscriberFromMap = function (name, subscriber) {      var subs = jps._getSubscribersFromMap(name);      var idx = subs.indexOf(subscriber);      if(idx > -1) {          subs.splice(idx, 1);      } else {          //do nothing      }      return subs;  };  /**  * remove all observed notification for the subscriber  */  jps._removeSubscribersFromMap = function (subscriber) {      var subs;      var names = jps._getInterestedNamesFromMap(subscriber);      names.forEach(function (name) {          var subs = jps._removeSubscriberFromMap(name, subscriber);           if(subs && subs.length === 0) {              delete jps._notificationMap[name];          }      });      return true;  };  jps._utils = {      'extendClass': function (child, parent) {          if(typeof child !== 'function')              throw new TypeError('extendClass child must be function type');          if(typeof parent !== 'function')              throw new TypeError('extendClass parent must be function type');          if(child === parent)              return ;          var Transitive = new Function();          Transitive.prototype = parent.prototype;          child.prototype = new Transitive();          return child.prototype.constructor = child;      }  };

这里的代码有点长,我们仔细来看一遍:

  • 首先是一个工具对象jps._utils,里面是有一些辅助方法,目前我们需要的类继承。
  • 其次是一个key/value的map:jps._notificationMap,用来建立订阅主题名与订阅者的映射。
  • 最后是管理者的实现。

    在这些实现中,我们看到,无非是增删改查。但是我们需要对于异常进行检查并处理。
    还有便是一个习惯的约定,以_开头的方法,一般表示private,私有成员。

封装为类库jpslib

这里,我们可以借助类似于webpack之类的工具进行代码的处理。
最后发布为npm
这里的详细过程就省略了,代码可以到github上获取。

当然此处可以使用ES6来写,会更易读一些,建议读者亲自去实现一遍,会有深刻体会。

封装一个组件基类

还是先上代码,我们拿React组件来举例:

'use strict';import React from 'react';import jpslib from 'jpslib';class ComSubscriber extends jpslib.ISubscriber {    constructor(name, callback, scope) {        super();        this._name = name;        this._callback = callback;        this._scope = scope;    }    get name() {        return this._name;    }    get callback() {        return this._callback;    }    listNotificationInterested() {        return [this._name];    }    executeNotification(notice) {        this._callback.call(this._scope || {}, notice.getBody());    }}class PSComponent extends React.Component {    constructor(props) {        super(props);        this._subscribers = [];    }    hasSubscriber(name, callback) {        for(let i = 0; i < this._subscribers.length; i++) {            let sub = this._subscribers[i];            if(sub.name === name && sub.callback === callback) {                return true;            }        }        return false;    }    addSubscriber(name, callback) {        if(typeof name === 'string') {            if(typeof callback === 'function') {                if(this.hasSubscriber(name, callback)) {                    return false;                }                let subscriber = new ComSubscriber(name, callback, this);                jpslib.subscribe(subscriber);                this._subscribers.push(subscriber);                return true;             } else {                throw new Error('addSubscriber parameter callback should be type of function');            }        } else {            throw new Error('addSubscriber parameter name should be type of string');        }    }    removeSubscriber(name, callback) {        if(typeof name === 'string' && typeof callback === 'function') {            for(let i = 0; i < this._subscribers.length; i++) {                let sub = this._subscribers[i];                if(sub.name === name && sub.callback === callback) {                    const subscriber = this._subscribers.splice(i, 1)[0];                    jpslib.unsubscribe(name, subscriber);                    return true;                }            }        } else if(typeof name === 'string' && callback === undefined) {            for(let i = 0; i < this._subscribers.length; i++) {                let sub = this._subscribers[i];                if(sub.name === name) {                    const subscirber = this._subscribers.splice(i, 1);                    jpslib.unsubscribe(name, subscriber);                    i--;                }            }            return true;        }        return false;    }    sendNotification(name, body) {        if(typeof name === 'string') {            const notice = jpslib.createNotification(name, body);            jpslib.publish(notice);        }    }}export default PSComponent;

所有继承自PSComponent的组件,便可以使用jpslib的通讯了,例如:

先创造一个消息名的枚举:

'use strict';class NoticeTypes {    static ICONBUTTON_CLICK = 'iconbutton_click';}export default NoticeTypes;

然后进行消息的订阅及回调处理:

'use strict';import React from 'react';import PSComponent from './pscomponent';import ComNotice from './notice';class App extends PSComponent {    constructor(props) {        super(props);    }    componentDidMount() {        this.addSubscriber(ComNotice.ICONBUTTON_CLICK, this._iconbuttonClickHandler);    }        componentWillUnmount() {        this.removeSubscriber(COMNotice.ICONBUTTON_CLICK, this._iconbuttonClickHandler);    }        _iconbuttonClickHandler() {        console.log('救命啊,我被点了');    }        render() {        ...    }};export default App;

在需要的地方广播消息:

this.sendNotification(COMNotice.ICONBUTTON_CLICK, {});

这样封装之后,是不是我们在写代码的时候就会发现好用很多了,简单清晰。


相关项目源码,请浏览github。
如有错误,还望不吝赐教。
内容均为原创,需要转载,请与本人联系。