订阅模式区别,订阅模式

时间: 2019-12-10阅读: 103标签: 模式

时间: 2019-05-24阅读: 312标签: 模式发布-订阅模式,看似陌生,其实不然。工作中经常会用到,例如 Node.js EventEmitter 中的 on 和 emit 方法;Vue 中的 $on 和 $emit 方法。他们都使用了发布订阅模式,让开发变得更加高效方便。一、 什么是发布-订阅模式1. 定义

转载自:http://www.cnblogs.com/lovesong/p/5272752.html

今天的话题是javascript中常被提及的「发布订阅模式和观察者模式」,提到这,我不由得想起了一次面试。记得在去年的一次求职面试过程中,面试官问我,“你在项目中是怎么处理非父子组件之间的通信的?”。我答道,“有用到vuex,有的场景也会用EventEmitter2”。面试官继续问,“那你能手写代码,实现一个简单的EventEmitter吗?”

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。

在翻阅资料的时候,有人把观察者(Observer)模式等同于发布(Publish)/订阅(Subscribe)模式,也有人认为这两种模式还是存在差异,而我认为确实是存在差异的,本质上的区别是调度的地方不同。
观察者模式
比较概念的解释是,目标和观察者是基类,目标提供维护观察者的一系列方法,观察者提供更新接口。具体观察者和具体目标继承各自的基类,然后具体观察者把自己注册到具体目标里,在具体目标发生变化时候,调度观察者的更新方法。
比如有个“天气中心”的具体目标A,专门监听天气变化,而有个显示天气的界面的观察者B,B就把自己注册到A里,当A触发天气变化,就调度B的更新方法,并带上自己的上下文。

手写EventEmitter

订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

图片 1

我犹豫了一会儿,想到使用EventEmitter2时,主要是用emit发事件,用on监听事件,还有off销毁事件监听者,removeAllListeners销毁指定事件的所有监听者,还有once之类的方法。考虑到时间关系,我想着就先实现发事件,监听事件,移除监听者这几个功能。当时可能有点紧张,不过有惊无险,在面试官给了一点提示后,顺利地写出来了!现在把这部分代码也记下来。

  1. 例子

发布/订阅模式
比较概念的解释是,订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心(顺带上下文),由调度中心统一调度订阅者注册到调度中心的处理代码。
比如有个界面是实时显示天气,它就订阅天气事件(注册到调度中心,包括处理程序),当天气变化时(定时获取数据),就作为发布者发布天气信息到调度中心,调度中心就调度订阅者的天气处理程序。

class EventEmitter { constructor() { // 维护事件及监听者 this.listeners = {} } /** * 注册事件监听者 * @param {String} type 事件类型 * @param {Function} cb 回调函数 */ on(type, cb) { if (!this.listeners[type]) { this.listeners[type] = [] } this.listeners[type].push(cb) } /** * 发布事件 * @param {String} type 事件类型 * @param {...any} args 参数列表,把emit传递的参数赋给回调函数 */ emit(type, ...args) { if (this.listeners[type]) { this.listeners[type].forEach(cb = { cb(...args) }) } } /** * 移除某个事件的一个监听者 * @param {String} type 事件类型 * @param {Function} cb 回调函数 */ off(type, cb) { if (this.listeners[type]) { const targetIndex = this.listeners[type].findIndex(item = item === cb) if (targetIndex !== -1) { this.listeners[type].splice(targetIndex, 1) } if (this.listeners[type].length === 0) { delete this.listeners[type] } } } /** * 移除某个事件的所有监听者 * @param {String} type 事件类型 */ offAll(type) { if (this.listeners[type]) { delete this.listeners[type] } }}// 创建事件管理器实例const ee = new EventEmitter()// 注册一个chifan事件监听者ee.on('chifan', function() { console.log('吃饭了,我们走!') })// 发布事件chifanee.emit('chifan')// 也可以emit传递参数ee.on('chifan', function(address, food) { console.log(`吃饭了,我们去${address}吃${food}!`) })ee.emit('chifan', '三食堂', '铁板饭') // 此时会打印两条信息,因为前面注册了两个chifan事件的监听者// 测试移除事件监听const toBeRemovedListener = function() { console.log('我是一个可以被移除的监听者') }ee.on('testoff', toBeRemovedListener)ee.emit('testoff')ee.off('testoff', toBeRemovedListener)ee.emit('testoff') // 此时事件监听已经被移除,不会再有console.log打印出来了// 测试移除chifan的所有事件监听ee.offAll('chifan')console.log(ee) // 此时可以看到ee.listeners已经变成空对象了,再emit发送chifan事件也不会有反应了

比如我们很喜欢看某个公众号号的文章,但是我们不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。

图片 2

有了这个自己写的简单版本的EventEmitter,我们就不用依赖第三方库啦。对了,vue也可以帮我们做这样的事情。

上面一个看似简单的操作,其实是一个典型的发布订阅模式,公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。

总结

const ee = new Vue();ee.$on('chifan', function(address, food) { console.log(`吃饭了,我们去${address}吃${food}!`) })ee.$emit('chifan', '三食堂', '铁板饭')

二、 如何实现发布-订阅模式?1. 实现思路创建一个对象在该对象上创建一个缓存列表(调度中心)on 方法用来把函数 fn 都加到缓存列表中(订阅者注册事件到调度中心)emit 方法取到 arguments 里第一个当做 event,根据 event 值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)remove 方法可以根据 event 值取消订阅(取消订阅)once 方法只监听一次,调用完毕后删除缓存函数(订阅一次)2. demo1

  1. 从两张图片可以看到,最大的区别是调度的地方。
    虽然两种模式都存在订阅者和发布者(具体观察者可认为是订阅者、具体目标可认为是发布者),但是观察者模式是由具体目标调度的,而发布/订阅模式是统一由调度中心调的,所以观察者模式的订阅者与发布者之间是存在依赖的,而发布/订阅模式则不会。
  2. 两种模式都可以用于松散耦合,改进代码管理和潜在的复用。
    附录
    观察者模式实现代码(JavaScript版):

所以我们可以单独new一个Vue的实例,作为事件管理器导出给外部使用。想测试的朋友可以直接打开vue官网,在控制台试试,也可以在自己的vue项目中实践下哦。

我们来看个简单的 demo,实现了 on 和 emit 方法,代码中有详细注释。

发布订阅模式

// 公众号对象let eventEmitter = {};// 缓存列表,存放 event 及 fneventEmitter.list = {};// 订阅eventEmitter.on = function (event, fn) { let _this = this; // 如果对象中没有对应的 event 值,也就是说明没有订阅过,就给 event 创建个缓存列表 // 如有对象中有相应的 event 值,把 fn 添加到对应 event 的缓存列表里 (_this.list[event] || (_this.list[event] = [])).push(fn); return _this;};// 发布eventEmitter.emit = function () { let _this = this; // 第一个参数是对应的 event 值,直接用数组的 shift 方法取出 let event = [].shift.call(arguments), fns = _this.list[event]; // 如果缓存列表里没有 fn 就返回 false if (!fns || fns.length === 0) { return false; } // 遍历 event 值对应的缓存列表,依次执行 fn fns.forEach(fn = { fn.apply(_this, arguments); }); return _this;};function user1 (content) { console.log('用户1订阅了:', content);};function user2 (content) { console.log('用户2订阅了:', content);};// 订阅eventEmitter.on('article', user1);eventEmitter.on('article', user2);// 发布eventEmitter.emit('article', 'Javascript 发布-订阅模式');/* 用户1订阅了: Javascript 发布-订阅模式 用户2订阅了: Javascript 发布-订阅模式*/
//观察者列表
function ObserverList(){
  this.observerList = [];
}
ObserverList.prototype.add = function( obj ){
  return this.observerList.push( obj );
};
ObserverList.prototype.count = function(){
  return this.observerList.length;
};
ObserverList.prototype.get = function( index ){
  if( index > -1 && index < this.observerList.length ){
    return this.observerList[ index ];
  }
};
ObserverList.prototype.indexOf = function( obj, startIndex ){
  var i = startIndex;
  while( i < this.observerList.length ){
    if( this.observerList[i] === obj ){
      return i;
    }
    i++;
  }
  return -1;
};
ObserverList.prototype.removeAt = function( index ){
  this.observerList.splice( index, 1 );
};

//目标
function Subject(){
  this.observers = new ObserverList();
}
Subject.prototype.addObserver = function( observer ){
  this.observers.add( observer );
};
Subject.prototype.removeObserver = function( observer ){
  this.observers.removeAt( this.observers.indexOf( observer, 0 ) );
};
Subject.prototype.notify = function( context ){
  var observerCount = this.observers.count();
  for(var i=0; i < observerCount; i++){
    this.observers.get(i).update( context );
  }
};

//观察者
function Observer(){
  this.update = function(){
    // ...
  };
}

其实仔细看看,EventEmitter就是一个典型的发布订阅模式,实现了事件调度中心。发布订阅模式中,包含发布者,事件调度中心,订阅者三个角色。我们刚刚实现的EventEmitter的一个实例ee就是一个事件调度中心,发布者和订阅者是松散耦合的,互不关心对方是否存在,他们关注的是事件本身。发布者借用事件调度中心提供的emit方法发布事件,而订阅者则通过on进行订阅。

  1. demo2

发布/订阅模式实现代码(JavaScript经典版):

如果还不是很清楚的话,我们把代码换下单词,是不是变得容易理解一点呢?

这一版中我们补充了一下 once 和 off 方法。

var pubsub = {};
(function(myObject) {
    // Storage for topics that can be broadcast
    // or listened to
    var topics = {};
    // An topic identifier
    var subUid = -1;
    // Publish or broadcast events of interest
    // with a specific topic name and arguments
    // such as the data to pass along
    myObject.publish = function( topic, args ) {
        if ( !topics[topic] ) {
            return false;
        }
        var subscribers = topics[topic],
            len = subscribers ? subscribers.length : 0;
        while (len--) {
            subscribers[len].func( topic, args );
        }
        return this;
    };
    // Subscribe to events of interest
    // with a specific topic name and a
    // callback function, to be executed
    // when the topic/event is observed
    myObject.subscribe = function( topic, func ) {
        if (!topics[topic]) {
            topics[topic] = [];
        }
        var token = ( ++subUid ).toString();
        topics[topic].push({
            token: token,
            func: func
        });
        return token;
    };
    // Unsubscribe from a specific
    // topic, based on a tokenized reference
    // to the subscription
    myObject.unsubscribe = function( token ) {
        for ( var m in topics ) {
            if ( topics[m] ) {
                for ( var i = 0, j = topics[m].length; i < j; i++ ) {
                    if ( topics[m][i].token === token ) {
                        topics[m].splice( i, 1 );
                        return token;
                    }
                }
            }
        }
        return this;
    };
}( pubsub ));
class PubSub { constructor() { // 维护事件及订阅行为 this.events = {} } /** * 注册事件订阅行为 * @param {String} type 事件类型 * @param {Function} cb 回调函数 */ subscribe(type, cb) { if (!this.events[type]) { this.events[type] = [] } this.events[type].push(cb) } /** * 发布事件 * @param {String} type 事件类型 * @param {...any} args 参数列表 */ publish(type, ...args) { if (this.events[type]) { this.events[type].forEach(cb = { cb(...args) }) } } /** * 移除某个事件的一个订阅行为 * @param {String} type 事件类型 * @param {Function} cb 回调函数 */ unsubscribe(type, cb) { if (this.events[type]) { const targetIndex = this.events[type].findIndex(item = item === cb) if (targetIndex !== -1) { this.events[type].splice(targetIndex, 1) } if (this.events[type].length === 0) { delete this.events[type] } } } /** * 移除某个事件的所有订阅行为 * @param {String} type 事件类型 */ unsubscribeAll(type) { if (this.events[type]) { delete this.events[type] } }}
let eventEmitter = { // 缓存列表 list: {}, // 订阅 on (event, fn) { let _this = this; // 如果对象中没有对应的 event 值,也就是说明没有订阅过,就给 event 创建个缓存列表 // 如有对象中有相应的 event 值,把 fn 添加到对应 event 的缓存列表里 (_this.list[event] || (_this.list[event] = [])).push(fn); return _this; }, // 监听一次 once (event, fn) { // 先绑定,调用后删除 let _this = this; function on () { _this.off(event, on); fn.apply(_this, arguments); } on.fn = fn; _this.on(event, on); return _this; }, // 取消订阅 off (event, fn) { let _this = this; let fns = _this.list[event]; // 如果缓存列表中没有相应的 fn,返回false if (!fns) return false; if (!fn) { // 如果没有传 fn 的话,就会将 event 值对应缓存列表中的 fn 都清空 fns  (fns.length = 0); } else { // 若有 fn,遍历缓存列表,看看传入的 fn 与哪个函数相同,如果相同就直接从缓存列表中删掉即可 fns.forEach((cb, i) = { if (cb === fn) { fns.splice(i, 1); } }); } return _this; }, // 发布 emit () { let _this = this; // 第一个参数是对应的 event 值,直接用数组的 shift 方法取出 let event = [].shift.call(arguments), fns = _this.list[event]; // 如果缓存列表里没有 fn 就返回 false if (!fns || fns.length === 0) { return false; } // 遍历 event 值对应的缓存列表,依次执行 fn fns.forEach(fn = { fn.apply(_this, arguments); }); return _this; }};function user1 (content) { console.log('用户1订阅了:', content);}function user2 (content) { console.log('用户2订阅了:', content);}function user3 (content) { console.log('用户3订阅了:', content);}function user4 (content) { console.log('用户4订阅了:', content);}// 订阅eventEmitter.on('article1', user1);eventEmitter.on('article1', user2);eventEmitter.on('article1', user3);// 取消user2方法的订阅eventEmitter.off('article1', user2);eventEmitter.once('article2', user4)// 发布eventEmitter.emit('article1', 'Javascript 发布-订阅模式');eventEmitter.emit('article1', 'Javascript 发布-订阅模式');eventEmitter.emit('article2', 'Javascript 观察者模式');eventEmitter.emit('article2', 'Javascript 观察者模式');// eventEmitter.on('article1', user3).emit('article1', 'test111');/* 用户1订阅了: Javascript 发布-订阅模式 用户3订阅了: Javascript 发布-订阅模式 用户1订阅了: Javascript 发布-订阅模式 用户3订阅了: Javascript 发布-订阅模式 用户4订阅了: Javascript 观察者模式*/

画图分析

三、 Vue 中的实现

最后,我们画个图加深下理解:

有了发布-订阅模式的知识后,我们来看下 Vue 中怎么实现 $on 和 $emit 的方法,直接看源码:

特点发布订阅模式中,对于发布者Publisher和订阅者Subscriber没有特殊的约束,他们好似是匿名活动,借助事件调度中心提供的接口发布和订阅事件,互不了解对方是谁。松散耦合,灵活度高,常用作事件总线易理解,可类比于DOM事件中的dispatchEvent和addEventListener。缺点当事件类型越来越多时,难以维护,需要考虑事件命名的规范,也要防范数据流混乱。观察者模式

function eventsMixin (Vue) { var hookRE = /^hook:/; Vue.prototype.$on = function (event, fn) { var this$1 = this; var vm = this; // event 为数组时,循环执行 $on if (Array.isArray(event)) { for (var i = 0, l = event.length; i  l; i++) { this$1.$on(event[i], fn); } } else { (vm._events[event] || (vm._events[event] = [])).push(fn); // optimize hook:event cost by using a boolean flag marked at registration // instead of a hash lookup if (hookRE.test(event)) { vm._hasHookEvent = true; } } return vm }; Vue.prototype.$once = function (event, fn) { var vm = this; // 先绑定,后删除 function on () { vm.$off(event, on); fn.apply(vm, arguments); } on.fn = fn; vm.$on(event, on); return vm }; Vue.prototype.$off = function (event, fn) { var this$1 = this; var vm = this; // all,若没有传参数,清空所有订阅 if (!arguments.length) { vm._events = Object.create(null); return vm } // array of events,events 为数组时,循环执行 $off if (Array.isArray(event)) { for (var i = 0, l = event.length; i  l; i++) { this$1.$off(event[i], fn); } return vm } // specific event var cbs = vm._events[event]; if (!cbs) { // 没有 cbs 直接 return this return vm } if (!fn) { // 若没有 handler,清空 event 对应的缓存列表 vm._events[event] = null; return vm } if (fn) { // specific handler,删除相应的 handler var cb; var i$1 = cbs.length; while (i$1--) { cb = cbs[i$1]; if (cb === fn || cb.fn === fn) { cbs.splice(i$1, 1); break } } } return vm }; Vue.prototype.$emit = function (event) { var vm = this; { // 传入的 event 区分大小写,若不一致,有提示 var lowerCaseEvent = event.toLowerCase(); if (lowerCaseEvent !== event  vm._events[lowerCaseEvent]) { tip( "Event "" + lowerCaseEvent + "" is emitted in component " + (formatComponentName(vm)) + " but the handler is registered for "" + event + "". " + "Note that HTML attributes are case-insensitive and you cannot use " + "v-on to listen to camelCase events when using in-DOM templates. " + "You should probably use "" + (hyphenate(event)) + "" instead of "" + event + ""." ); } } var cbs = vm._events[event]; if (cbs) { cbs = cbs.length  1 ? toArray(cbs) : cbs; // 只取回调函数,不取 event var args = toArray(arguments, 1); for (var i = 0, l = cbs.length; i  l; i++) { try { cbs[i].apply(vm, args); } catch (e) { handleError(e, vm, ("event handler for "" + event + """)); } } } return vm };}/*** * Convert an Array-like object to a real Array. */function toArray (list, start) { start = start || 0; var i = list.length - start; var ret = new Array(i); while (i--) { ret[i] = list[i + start]; } return ret}

观察者模式与发布订阅模式相比,耦合度更高,通常用来实现一些响应式的效果。在观察者模式中,只有两个主体,分别是目标对象Subject,观察者Observer。

实现思路大体相同,如上第二点中的第一条:实现思路。Vue 中实现的方法支持订阅数组事件。

观察者需Observer要实现update方法,供目标对象调用。update方法中可以执行自定义的业务代码。目标对象Subject也通常被叫做被观察者或主题,它的职能很单一,可以理解为,它只管理一种事件。Subject需要维护自身的观察者数组observerList,当自身发生变化时,通过调用自身的notify方法,依次通知每一个观察者执行update方法。

四、 总结1. 优点对象之间解耦异步编程中,可以更松耦合的代码编写2. 缺点创建订阅者本身要消耗一定的时间和内存虽然可以弱化对象之间的联系,多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护五、 扩展(发布-订阅模式与观察者模式的区别)

按照这种定义,我们可以实现一个简单版本的观察者模式。

很多地方都说发布-订阅模式是观察者模式的别名,但是他们真的一样吗?是不一样的。直接上图:

// 观察者class Observer { /** * 构造器 * @param {Function} cb 回调函数,收到目标对象通知时执行 */ constructor(cb){ if (typeof cb === 'function') { this.cb = cb } else { throw new Error('Observer构造器必须传入函数类型!') } } /** * 被目标对象通知时执行 */ update() { this.cb() }}// 目标对象class Subject { constructor() { // 维护观察者列表 this.observerList = [] } /** * 添加一个观察者 * @param {Observer} observer Observer实例 */ addObserver(observer) { this.observerList.push(observer) } /** * 通知所有的观察者 */ notify() { this.observerList.forEach(observer = { observer.update() }) }}const observerCallback = function() { console.log('我被通知了')}const observer = new Observer(observerCallback)const subject = new Subject();subject.addObserver(observer);subject.notify();

观察者模式:观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。

画图分析

发布订阅模式:订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

最后也整张图理解下观察者模式:

差异:

特点角色很明确,没有事件调度中心作为中间者,目标对象Subject和观察者Observer都要实现约定的成员方法。双方联系更紧密,目标对象的主动性很强,自己收集和维护观察者,并在状态变化时主动通知观察者更新。缺点

在观察者模式中,观察者是知道 Subject 的,Subject 一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。观察者模式大多数时候是同步的,比如当事件触发,Subject 就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。

我还没体会到,这里不做评价

原文来自:

结语

关于这个话题,网上文章挺多的,观点上可能也有诸多分歧。重复造轮子,纯属帮助自己加深理解。

本人水平有限,以上仅是个人观点,如有错误之处,还请斧正!如果能帮到您理解发布订阅模式和观察者模式,非常荣幸!

如果有兴趣看看我这糟糕的代码,请点击github,祝大家生活愉快!首发链接

本文由澳门威斯尼人平台登录发布于Web前端,转载请注明出处:订阅模式区别,订阅模式

相关阅读