乐趣区

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

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

观察者模式

概念

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

实现

这个实现包含以下组件:

  • 被观察者:维护一组观察者,提供用于增加和移除观察者的方法
  • 观察者:提供一个更新接口,用于当被观察者状态变化时,得到通知
  • 具体的被观察者:状态变化时广播通知给观察者,保持具体的观察者的信息
  • 具体的观察者:保持一个指向具体被观察者的引用,实现一个更新接口,用于观察,以便保证自身状态总是和被观察者状态一致的
  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. 设计模式(三):观察者模式与发布 / 订阅模式区别
退出移动版