觀察者模式與發佈/訂閱模式

觀察者模式與發佈/訂閱模式

觀察者模式

概念

一個被觀察者的對象,經過註冊的方式維護一組觀察者對象。當被觀察者發生變化,就會產生一個通知,經過廣播的方式發送出去,最後調用每一個觀察者的更新方法。當觀察者再也不須要接受被觀察者的通知時,被觀察者能夠將該觀察者從所維護的組中刪除。html

實現

這個實現包含如下組件:ajax

  • 被觀察者:維護一組觀察者, 提供用於增長和移除觀察者的方法
  • 觀察者:提供一個更新接口,用於當被觀察者狀態變化時,獲得通知
  • 具體的被觀察者:狀態變化時廣播通知給觀察者,保持具體的觀察者的信息
  • 具體的觀察者:保持一個指向具體被觀察者的引用,實現一個更新接口,用於觀察,以便保證自身狀態老是和被觀察者狀態一致的
  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 函數以後會被具體的行爲覆蓋app

    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) // 設置導航模塊的頭像
})

使用了發佈/訂閱模式的代碼this

$.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. 發佈/訂閱模式弱化對象之間的聯繫,對象和對象之間的必要聯繫也被深埋在背後,致使程序難以跟蹤維護和理解

兩者的不一樣

  • 觀察者模式要求想要接受相關通知的觀察者必須到發起這個事件的被觀察者上註冊這個事件

    觀察者m's

    controlCheckbox.AddObserver(check)
  • 發佈/訂閱模式使用一個主題/事件頻道,這個頻道處於訂閱者和發佈者之間,這個事件系統容許代碼定義應用相關的事件,避免訂閱者和發佈者之間的依賴性

    發佈/訂閱模式

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

參考資料

  1. 《JavaScript設計模式》 做者:Addy Osmani
  2. 《JavaScript設計模式與開發實踐》 做者:曾探
  3. 設計模式(三):觀察者模式與發佈/訂閱模式區別
相關文章
相關標籤/搜索