提及发布订阅,我们都知道是一种比较经典的设计模式。比如像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。
如有错误,还望不吝赐教。
内容均为原创,需要转载,请与本人联系。