JS 觀察者模式

SHERlocked93發表於2017-12-22

1. 介紹

觀察者模式又叫釋出訂閱模式(Publish/Subscribe),它定義了一種一對多的關係,讓多個觀察者物件同時監聽某一個主題物件,這個主題物件的狀態發生變化時就會通知所有的觀察者物件,使得它們能夠自動更新自己。

使用觀察者模式的好處:

  1. 支援簡單的廣播通訊,自動通知所有已經訂閱過的物件。
  2. 目標物件與觀察者存在的是動態關聯,增加了靈活性。
  3. 目標物件與觀察者之間的抽象耦合關係能夠單獨擴充套件以及重用。

2. 實現一

如下例子:

  • subscribers:含有不同 type 的陣列,儲存有所有訂閱者的陣列,訂閱行為將被加入到這個陣列中
  • subscribe:方法為將訂閱者新增到 subscribers 中對應的陣列中
  • unsubscribe:方法為在 subscribers 中刪除訂閱者
  • publish:迴圈遍歷 subscribers 中的每個元素,並呼叫他們註冊時提供的方法
let publisher = {
  subscribers: {
    any: []
  },
  subscribe: function(fn, type = 'any') {
    if (typeof this.subscribers[type] === 'undefined') {
      this.subscribers[type] = []
    }
    this.subscribers[type].push(fn)
  },
  unsubscribe: function(fn, type) {
    this.visitSubscribers('unsubscribe', fn, type)
  },
  publish: function(publication, type) {
    this.visitSubscribers('publish', publication, type)
  },
  visitSubscribers: function(action, arg, type = 'any') {
    this.subscribers[type].forEach((currentValue, index, array) => {
      if (action === 'publish') {
        currentValue(arg)
      } else if (action === 'unsubscribe') {
        if (currentValue === arg) {
          this.subscribers[type].splice(index, 1)
        }
      }
    })
  }
}

let funcA = function(cl) {
  console.log('msg1' + cl)
}
let funcB = function(cl) {
  console.log('msg2' + cl)
}

publisher.subscribe(funcA)
publisher.subscribe(funcB)
publisher.unsubscribe(funcB)

publisher.publish(' in publisher')     // msg1 in publisher       

這裡可以通過一個函式 makePublisher() 將一個物件複製成 publisher ,從而將其轉換成一個釋出者。

function makePublisher(o) {
  Object.keys(publisher).forEach((curr, index, array) => {
    if (publisher.hasOwnProperty(curr) && typeof publisher[curr] === 'function') {
      o[curr] = publisher[curr]
    }
  })
  o.subscribers={any:[]}
}

// 發行者物件
let paper = {
  daily: function() {
    this.publish('big news today')
  },
  monthly: function() {
    this.publish('interesting analysis', 'monthly')
  }
}

makePublisher(paper)

// 訂閱物件
let joe = {
  drinkCoffee: function(paper) {
    console.log('Just read daily ' + paper)
  },
  sundayPreNap: function(monthly) {
    console.log('Reading this monthly ' + monthly)
  }
}

paper.subscribe(joe.drinkCoffee)
paper.subscribe(joe.sundayPreNap, 'monthly')

paper.daily()         // Just read daily big news today
paper.monthly()         // Reading this monthly interesting analysis

3. 實現二

使用ES6裡的class稍微改造下:

class publisher {
    constructor() {
        this.subscribers = {
            any: []
        }
    }
    subscribe(fn, type = 'any') {
        if (typeof this.subscribers[type] === 'undefined') {
            this.subscribers[type] = []
        }
        this.subscribers[type].push(fn)
    }
    unsubscribe(fn, type) {
        this.visitSubscribers('unsubscribe', fn, type)
    }
    publish(publication, type) {
        this.visitSubscribers('publish', publication, type)
    }
    visitSubscribers(action, arg, type = 'any') {
        this.subscribers[type].forEach((currentValue, index, array) => {
            if (action === 'publish') {
                currentValue(arg)
            } else if (action === 'unsubscribe') {
                if (currentValue === arg) {
                    this.subscribers[type].splice(index, 1)
                }
            }
        })
    }
}

let publish = new publisher();

let funcA = function(cl) {
    console.log('msg1' + cl)
}
let funcB = function(cl) {
    console.log('msg2' + cl)
}

publish.subscribe(funcA)
publish.subscribe(funcB)
publish.unsubscribe(funcB)

publish.publish(' in publisher')     // msg1 in publisher

4. 實現三

以上兩個方法都是《JavaScript模式》裡介紹的,這裡貼上個自己實現的,感覺看起來舒服點...

  • 使用IIFE的方法:
const Observer = (function() {
  const _message = {}  // 訊息佇列
  return {
    regist(type, fn) {          // 訂閱
      _message[type]
          ? _message[type].push(fn)
          : _message[type] = [fn]
    },
    emit(type, payload) {          // 釋出
      if (!_message[type]) {
        return
      }
      _message[type].forEach(event => event(payload))
    },
    remove(type, fn) {            // 退訂
      if (!_message[type].includes(fn)) {return}
      const idx = _message[type].indexOf(fn)
      _message[type].splice(idx, 1)
    }
  }
})()
  • 使用ES6的class方法
class Observer {
  constructor() {
    this._message = {}
  }
  
  regist(type, fn) {          // 訂閱
    this._message[type]
        ? this._message[type].push(fn)
        : this._message[type] = [fn]
  }
  
  emit(type, payload) {          // 釋出
    if (!this._message[type]) {
      return
    }
    this._message[type].forEach(event => event(payload))
  }
  
  remove(type, fn) {            // 退訂
    if (!this._message[type].includes(fn)) {return}
    const idx = this._message[type].indexOf(fn)
    this._message[type].splice(idx, 1)
  }
}

5. 總結

觀察者的使用場合就是:當一個物件的改變需要同時改變其它物件,並且它不知道具體有多少物件需要改變的時候,就應該考慮使用觀察者模式。

總的來說,觀察者模式所做的工作就是在解耦,讓耦合的雙方都依賴於抽象,而不是依賴於具體。從而使得各自的變化都不會影響到另一邊的變化。


本文是系列文章,可以相互參考印證,共同進步~

  1. JS 抽象工廠模式
  2. JS 工廠模式
  3. JS 建造者模式
  4. JS 原型模式
  5. JS 單例模式
  6. JS 回撥模式
  7. JS 外觀模式
  8. JS 介面卡模式
  9. JS 利用高階函式實現函式快取(備忘模式)
  10. JS 狀態模式
  11. JS 橋接模式
  12. JS 觀察者模式

網上的帖子大多深淺不一,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~

參考:
設計模式之觀察者模式
《JavaScript模式》
《Javascript 設計模式》 - 張榮銘

相關文章