观察者模式与发布订阅模式

44次阅读

共计 5552 个字符,预计需要花费 14 分钟才能阅读完成。

观察者模式与发布 / 订阅模式

观察者模式

概念

一个被观察者的对象,通过注册的方式维护一组观察者对象。当被观察者发生变化,就会产生一个通知,通过广播的方式发送出去,最后调用每个观察者的更新方法。当观察者不再需要接受被观察者的通知时,被观察者可以将该观察者从所维护的组中删除。

实现

这个实现包含以下组件:

  • 被观察者:维护一组观察者,提供用于增加和移除观察者的方法
  • 观察者:提供一个更新接口,用于当被观察者状态变化时,得到通知
  • 具体的被观察者:状态变化时广播通知给观察者,保持具体的观察者的信息
  • 具体的观察者:保持一个指向具体被观察者的引用,实现一个更新接口,用于观察,以便保证自身状态总是和被观察者状态一致的
  1. 首先,对被观察者维护的一组观察者(列表)进行建模

    function ObserverList() {this.observerList = []
    }
    
    ObserverList.prototype.add = function(obj) {return this.observerList.push(obj)
    }
    
    ObserverList.prototype.Empty = function() {this.observerList = []
    }
    
    ObserverList.prototype.removeAt = function(index) {this.observerList.splice(index, 1)
    }
    
    ObserverList.prototype.count = function() {return this.observerList.length}
    
    ObserverList.prototype.get = function(index) {if (index > -1 && index < this.observerList.length) {return this.observerList[index]
      }
    }
    
    // Extend an object with an extension
    function extend(extension, obj) {for (var key in extension) {obj[key] = extension[key]
      }
    }
  2. 接着,对被观察者以及其增加、删除、通知能力进行建模

    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)
      }
    }
  3. 接着,对观察者进行建模,这里的 update 函数之后会被具体的行为覆盖

    function Observer() {this.update = function() {// ...}
    }

样例应用

我们使用上面的观察者组件,现在我们定义

  • 一个按钮,这个按钮用于增加新的充当观察者的选择框到页面上
  • 一个控制用的选择框 , 充当一个被观察者,通知其它选择框是否应该被选中
  • 一个容器,用于放置新的选择框

    <button id="addNewObserver">Add New Observer checkbox</button>
    <input id="mainCheckbox" type="checkbox"/>
    <div id="observersContainer"></div>
    // DOM 元素的引用
    var controlCheckbox = document.getElementById('mainCheckbox'),
      addBtn = document.getElementById('addNewObserver'),
      container = document.getElementById('observersContainer')
    
    // 具体的被观察者
    
    // Subject 类扩展 controlCheckbox
    extend(new Subject(), controlCheckbox)
    
    // 点击 checkbox 将会触发对观察者的通知
    controlCheckbox['onclick'] = new Function('controlCheckbox.notify(controlCheckbox.checked)'
    )
    
    addBtn['onclick'] = AddNewObserver
    
    // 具体的观察者
    
    function AddNewObserver() {
      // 建立一个新的用于增加的 checkbox
      var check = document.createElement('input')
      check.type = 'checkbox'
    
      // 使用 Observer 类扩展 checkbox
      extend(new Observer(), check)
    
      // 使用定制的 update 函数重载
      check.update = function(value) {this.checked = value}
    
      // 增加新的观察者到我们主要的被观察者的观察者列表中
      controlCheckbox.AddObserver(check)
    
      // 将元素添加到容器的最后
      container.appendChild(check)
    }

上述示例中

  • Subject 类扩展 controlCheckbox,所以 controlCheckbox 是具体的被观察者
  • 点击 addBtn 时,生成一个新的 check,check 被 Observer 类所拓展并重写了 update 方法,所以 check 是具体的观察者,最后 controlCheckbox 将 check 添加到了 controlCheckbox 所维护的观察者列表中
  • 点击 controlCheckbox,调用了被观察者的 notify 方法,进而触发了 controlCheckbox 中所维护的观察者的 update 方法

发布 / 订阅模式

实现

var pubsub = {}

;(function(q) {var topics = {},
    subUid = -1

  // Publish or broadcast events of interest
  // with a specific topic name and arguments
  // such as the data to pass along
  q.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
  q.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
  q.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)

样例应用 1

// Another simple message handler

// A simple message logger that logs any topics and data received through our
// subscriber
var messageLogger = function(topics, data) {console.log('Logging:' + topics + ':' + data)
}

// Subscribers listen for topics they have subscribed to and
// invoke a callback function (e.g messageLogger) once a new
// notification is broadcast on that topic
var subscription = pubsub.subscribe('inbox/newMessage', messageLogger)

// Publishers are in charge of publishing topics or notifications of
// interest to the application. e.g:

pubsub.publish('inbox/newMessage', 'hello world!')

// or
pubsub.publish('inbox/newMessage', ['test', 'a', 'b', 'c'])

// or
pubsub.publish('inbox/newMessage', {
  sender: 'hello@google.com',
  body: 'Hey again!'
})

// We cab also unsubscribe if we no longer wish for our subscribers
// to be notified
// pubsub.unsubscribe(subscription);

// Once unsubscribed, this for example won't result in our
// messageLogger being executed as the subscriber is
// no longer listening
pubsub.publish('inbox/newMessage', 'Hello! are you still there?')

样例应用 2

旧的代码

$.ajax('http:// xxx.com?login', function(data) {header.setAvatar(data.avatar) // 设置 header 模块的头像
  nav.setAvatar(data.avatar) // 设置导航模块的头像
})

使用了发布 / 订阅模式的代码

$.ajax('http:// xxx.com?login', function(data) {pubsub.publish('loginSucc', data) // 发布登录成功的消息
})

// header 模块
var header = (function() {pubsub.subscribe('loginSucc', function(data) {header.setAvatar(data.avatar)
  })

  return {setAvatar: function(data) {console.log('设置 header 模块的头像')
    }
  }
})()

// nav 模块
var nav = (function() {pubsub.subscribe('loginSucc', function(data) {nav.setAvatar(data.avatar)
  })

  return {setAvatar: function(avatar) {console.log('设置 nav 模块的头像')
    }
  }
})()

优点

  1. 可以应用于异步编程中,比如 ajax 请求的 succ、error 等事件中,或者动画的每一帧完成之后去发布一个事件,从而不需要过多关注对象再异步运行期间的内部状态
  2. 取代对象之间硬编码的通知机制,一个对象不再显示地调用另外一个对象的某个接口,让这两个对象松耦合的联系在一起

缺点

  1. 创建订阅者需消耗一定内存,当你订阅一个消息后,即使消息直到最后都未发生,但这个订阅者也会始终存在于内存中
  2. 发布 / 订阅模式弱化对象之间的联系,对象和对象之间的必要联系也被深埋在背后,导致程序难以跟踪维护和理解

二者的不同

  • 观察者模式要求想要接受相关通知的观察者必须到发起这个事件的被观察者上注册这个事件

    controlCheckbox.AddObserver(check)
  • 发布 / 订阅模式使用一个主题 / 事件频道,这个频道处于订阅者和发布者之间,这个事件系统允许代码定义应用相关的事件,避免订阅者和发布者之间的依赖性

    pubsub.subscribe('inbox/newMessage', messageLogger)
    pubsub.publish('inbox/newMessage', 'hello world!')

参考资料

  1. 《JavaScript 设计模式》作者:Addy Osmani
  2. 《JavaScript 设计模式与开发实践》作者:曾探
  3. 设计模式(三):观察者模式与发布 / 订阅模式区别

正文完
 0

观察者模式与发布-订阅模式

46次阅读

共计 2551 个字符,预计需要花费 7 分钟才能阅读完成。

观察者模式(Observer)
观察者模式:一个对象(主体)根据它维护的一个对象列表(观察者),自动通知它们状态的任何变化。(举例说明,电商平台关注(订阅)一家店铺(发布者)的鞋子,当鞋子上架之后店铺就会发送消息给用户(订阅者);用户(订阅者)可以通过取消订阅来取消推送消息接收。)
实际上是:主体对观察者传递消息通知,观察者必须将该消息通知订阅到触发事件对象上。
关系链:目标 <— 继承 — 具体目标(发布者)< — 订阅 — 调度观察者 — > 具体观察者(订阅者)— 继承 —> 观察者
具体代码实现:
// 观察者列表
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(){
// 自定义行为
};
}
发布 - 订阅模式
发布订阅模式:观察者模式也可以说是发布订阅模式,然而发布订阅模式与观察者模式不同之处就在于,添加了一个中介(调度中心)来避免发布者和订阅者之间产生依赖关系。
关系链:订阅者(数量不限)— 订阅 —> 调度中心 <— 发布 — 发布者(不直接发布)
两种模式的区别:观察两种模式其实发现,模式的基本思想是一致的,仅仅是在调度是否是直接调度上有所不同。观察者模式是由具体目标直接调度的(eg: dom 操作);而发布订阅模式是在调度中心调度,发布者与订阅者不产生依赖。
具体代码实现:
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));
参考文献:《Learning JavaScript Design Patterns》

正文完
 0