javascript(js) 觀察者模式和釋出訂閱模式

胖鵝68發表於2020-12-17

參考文件

  1. 視訊參考
  2. 觀察者模式和釋出訂閱模式(JS)
  3. JavaScript 釋出-訂閱模式

問題描述

最近想學習一下Vue 原始碼,在設定$data的值的時候,是如何通知模板變化的,其中就用到了 “訂閱-釋出”模式,發現對此思路不是很清晰,因此寫個學習筆記加強鞏固

觀察者模式

觀察者模式,目標和觀察者是基類,目標提供維護觀察者的一系列方法,觀察者提供更新介面。具體觀察者和具體目標繼承各自的基類,然後具體觀察者把自己註冊到具體目標裡,在具體目標發生變化時候,排程觀察者的更新方法
個人理解就是目標發生變化通知觀察者,即觀察者模式是由具體目標排程的

比如有個“天氣中心”的具體目標A,專門監聽天氣變化,而有個顯示天氣的介面的觀察者B,B就把自己註冊到A裡,當A觸發天氣變化,就排程B的更新方法,並帶上自己的上下文。

在這裡插入圖片描述

觀察者模式 demo

class 描述觀察者模式

// 定義主題,即觀察者觀察的物件(目標)
class Subject {
  constructor (name) {
    this.name = name
    this.observers = [] // 用來儲存觀察者
    this.state = '心情好'
  }
  // 新增觀察者
  attach (observer) {
    this.observers.push(observer)
  }

  // 一旦被觀察的資訊發生變化,則通知觀察者
  setState (newState) {
    this.state = newState
    this.observers.forEach(observer => {
      observer.update(newState)
    })
  }
}
// 定義觀察者
class Observer {
  constructor (name) {
    this.name = name
  }
  // 定義觀察者針對資料發生變化做出的響應
  update (newState) {
    console.log(this.name + '處理:' + newState)
  }
}

const subject = new Subject('我是新聞')
const observerOne = new Observer('我是讀者11')
const observerTwo = new Observer('我是讀者22')
subject.attach(observerOne)
subject.attach(observerTwo)
subject.setState('改變新聞了')

事件的觀察者模式

class Events {
  constructor () {
    this.callbacks = []
  }
  // 訂閱
  on (callback) {
    this.callbacks.push(callback)
  }
  // 釋出
  emit (...data) {
    this.callbacks.forEach(fn => {
      fn(...data)
    })
  }
}

const eventBus = new Events()

eventBus.on(function (...data) {
  console.log('監聽到資料變化1')
  if (data.length === 1) {
    console.log('我處理一個資料' + data)
  }
})

eventBus.on(function (...data) {
  console.log('監聽到資料變化2')
  if (data.length === 2) {
    console.log('我處理2個資料' + data)
  }
})

eventBus.on(function (...data) {
  console.log('監聽到資料變化3')
  if (data.length > 2) {
    console.log('我處理2個以上的資料' + data)
  }
})

eventBus.emit('one')
eventBus.emit('two', 'param2')
eventBus.emit('three', 'param2', 'param3')

陣列塌陷

let _subscribe = (function () {
  class Sub {
    constructor () {
      // 1. 建立一個事件池,用來儲存後期需要執行的方法
      this.$pond = []
    }
    // 2. 向事件池中追加方法(重複處理)
    add (func) {
      let flag = this.$pond.some(item => {
        return item === func
      })
      !flag ? this.$pond.push(func) : null
    }
    // 3. 從事件池中移除方法
    remove (func) {
      let $pond = this.$pond
      for (let i = 0; i < $pond.length; i++) {
        let item = $pond[i]
        if (item === func) {
          // 移除 (順序不變的情況下基本上只能用splice了)
          // 不能這樣寫,會導致陣列塌陷問題,我們不能真移除,只能把當前項賦值為 Null
          // $pond.splice(i, 1)
          $pond[i] = null
          break
        }
      }
    }
    // 4. 通知事件池中的方法,按照順序依次執行
    fire (...args) {
      console.log(arguments)
      console.log(args)
      const $pond = this.$pond
      for (let i = 0; i < $pond.length; i++) {
        let item = $pond[i]
        if (typeof item !== 'function') {
          // 此時再刪除
          $pond.splice(i, 1)
          i--
          continue
        }
        item.call(this, ...args)
      }
    }
  }
  // 暴露給外面的使用
  return function () {
    return new Sub()
  }
}())

// 測試
function test () {
  let s1 = _subscribe()
  let fn1 = function () {
    console.log(1)
  }
  let fn2 = function () {
    console.log(2)
    // 模擬陣列塌陷的現象
    s1.remove(fn1)
  }
  s1.add(fn1)
  s1.add(fn2)
  s1.add(fn1)
  let fn3 = function () {
    console.log(3)
  }
  let fn4 = function () {
    console.log(4)
  }
  s1.add(fn3)
  s1.add(fn4)
  s1.fire('huang', 'biao')
}

test()

釋出/訂閱模式

釋出/訂閱模式,訂閱者把自己想訂閱的事件註冊到排程中心,當該事件觸發時候,釋出者釋出該事件到排程中心(順帶上下文),由排程中心統一排程訂閱者註冊到排程中心的處理程式碼。

比如有個介面是實時顯示天氣,它就訂閱天氣事件(註冊到排程中心,包括處理程式),當天氣變化時(定時獲取資料),就作為釋出者釋出天氣資訊到排程中心,排程中心就排程訂閱者的天氣處理程式。

個人理解:可以選擇主題,觸發關心該主題的訂閱者
在這裡插入圖片描述

釋出訂閱的demo

// 釋出、訂閱模式

let _subscribe = function () {
  var pubsub = {
    // 用來儲存 “主題” 和 訂閱者物件
    topics: {},
    // 記錄每個訂閱者物件的唯一標識
    subUid: -1,
    // 釋出指定訂閱
    publish: function (topic, ...args) {
      if (!this.topics[topic]) {
        return false
      }

      var subscribers = this.topics[topic]
      var len = subscribers ? subscribers.length : 0
      while (len--) {
        subscribers[len].func(topic, ...args)
      }
      return this
    },
    // 向訂閱中心新增訂閱
    subscribe: function (topic, func) {
      // 如果之前沒有新增 “主題”,則新增一個主題,值為陣列,用來儲存訂閱者回撥函式
      if (!this.topics[topic]) {
        this.topics[topic] = []
      }
      var token = (++this.subUid).toString()
      // 主題下面有多個訂閱者,允許重複,token 為訂閱者的唯一標識
      this.topics[topic].push({
        token: token,
        func: func
      })
      return token
    },
    // 向訂閱中移除訂閱
    unSubscribe: function (token) {
      // 遍歷所有的主題
      for (var m in this.topics) {
        // 主題存在值
        if (this.topics[m]) {
          // 遍歷主題的每個訂閱者,根據token找到唯一的訂閱者的值,然後去掉
          for (var i = 0, j = this.topics[m].length; i < j; i++) {
            if (this.topics[m][i].token === token) {
              // 使用 splice 同樣會存在陣列塌陷的問題
              this.topics[m].splice(i, 1)
              // 將刪除的訂閱者設定為 null, 再執行訂閱者處理響應的時候刪除掉相關訂閱者
              // this.topics[m] = null
              return token
            }
          }
        }
      }
      return this
    }
  }
  return pubsub
}

function testAction () {
  const pubsub = _subscribe()
  console.log(pubsub.subscribe('test', (...args) => {
    console.log('hello world1:' + args)
    // pubsub.unSubscribe(0)
  }))
  console.log(pubsub.subscribe('test', (...args) => {
    console.log('hello world2:' + args)
    // pubsub.unSubscribe(1)
  }))
  console.log(pubsub.subscribe('test', (...args) => {
    console.log('hello world3:' + args)
    pubsub.unSubscribe(2)
  }))
  console.log(pubsub.subscribe('two', (...args) => { console.log('hello two:' + args) }))
  console.log(pubsub.subscribe('two', (...args) => { console.log('hello two:' + args) }))
  console.log(pubsub.subscribe('two', (...args) => { console.log('hello two:' + args) }))
  console.log(pubsub.subscribe('two', (...args) => { console.log('hello two:' + args) }))
  pubsub.publish('test', 'huang', 'biao', 18)

  setTimeout(() => {
    pubsub.publish('two', 'huang', 'biao', 18)
  }, 2000)
}

testAction()

Vue的事件管理函式$on && $emit && $off

模擬事件管理器

function EventEmitter () {
  let emitter = {
    // 快取列表
    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 與哪個函式相同,如果相同就直接從快取列表中刪掉即可
        let cb
        for (let i = 0, cbLen = fns.length; i < cbLen; i++) {
          cb = fns[i]
          if (cb === fn || cb.fn === fn) {
            fns.splice(i, 1)
            break
          }
        }
      }
      return _this
    },
    // 釋出
    emit () {
      let _this = this
      // 第一個引數是對應的 event 值,直接用陣列的 shift 方法取出
      let event = [].shift.call(arguments)
      let fns = [..._this.list[event]]
      // 如果快取列表裡沒有 fn 就返回 false
      if (!fns || fns.length === 0) {
        return false
      }
      // 遍歷 event 值對應的快取列表,依次執行 fn
      fns.forEach(fn => {
        fn.apply(_this, arguments)
      })
      return _this
    }
  }
  return emitter
}

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)
}

function testAction () {
  let eventEmitter = EventEmitter()
  // 訂閱
  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 釋出-訂閱模式', 'param1', 'param2')
  eventEmitter.emit('article1', 'Javascript 釋出-訂閱模式', 'param1', 'param2')
  eventEmitter.emit('article2', 'Javascript 觀察者模式', 'param1', 'param2')
  eventEmitter.emit('article2', 'Javascript 觀察者模式', 'param1', 'param2')
}

testAction()

// eventEmitter.on('article1', user3).emit('article1', 'test111');

/*
使用者1訂閱了: [ 'Javascript 釋出-訂閱模式', 'param1', 'param2' ]
使用者3訂閱了: [ 'Javascript 釋出-訂閱模式', 'param1', 'param2' ]
使用者1訂閱了: [ 'Javascript 釋出-訂閱模式', 'param1', 'param2' ]
使用者3訂閱了: [ 'Javascript 釋出-訂閱模式', 'param1', 'param2' ]
使用者4訂閱了: [ 'Javascript 觀察者模式', 'param1', 'param2' ]
*/

釋出-訂閱模式與觀察者模式的區別

在這裡插入圖片描述

觀察者模式:觀察者(Observer)直接訂閱(Subscribe)主題(Subject),而當主題被啟用的時候,會觸發(Fire Event)觀察者裡的事件。

釋出訂閱模式:訂閱者(Subscriber)把自己想訂閱的事件註冊(Subscribe)到排程中心(Event Channel),當釋出者(Publisher)釋出該事件(Publish Event)到排程中心,也就是該事件觸發時,由排程中心統一排程(Fire Event)訂閱者註冊到排程中心的處理程式碼。

差異

  1. 在觀察者模式中,觀察者是知道 Subject 的,Subject 一直保持對觀察者進行記錄。然而,在釋出訂閱模式中,釋出者和訂閱者不知道對方的存在。它們只有通過訊息代理進行通訊。
  2. 在釋出訂閱模式中,元件是鬆散耦合的,正好和觀察者模式相反。
  3. 觀察者模式大多數時候是同步的,比如當事件觸發,Subject 就會去呼叫觀察者的方法。而釋出-訂閱模式大多數時候是非同步的(使用訊息佇列)。
  4. 觀察者模式需要在單個應用程式地址空間中實現,而釋出-訂閱更像交叉應用模式。

相關文章