發(fā)布-訂閱模式

發(fā)布—訂閱模式又叫觀察者模式绢慢,它定義對象間的一種一對多的依賴關(guān)系,當(dāng)一個對象的狀態(tài)發(fā)生改變時成黄,所有依賴于它的對象都將得到通知呐芥。它有兩個應(yīng)用場景:

  • 可以廣泛應(yīng)用于異步編程中逻杖,替代回調(diào)函數(shù)奋岁。
  • 一個對象不用再顯式的調(diào)用另一個對象的接口。

讓兩個對象松耦合地聯(lián)系在一起荸百,雖然不太清楚彼此的細(xì)節(jié)闻伶,但這不影響它們之間相互通信。當(dāng)有新的訂閱者出現(xiàn)時够话,發(fā)布者的代碼不需要任何修改蓝翰;同樣發(fā)布者需要改變時,也不會影響到之前的訂閱者女嘲。只要之前約定的事件名沒有變化畜份,就可以自由地改變它們。DOM事件綁定就是典型的發(fā)布-訂閱模式欣尼。

下面用訂房的例子來實現(xiàn)發(fā)布-訂閱模式爆雹。

第一版

訂閱者根據(jù)key來訂閱自己感興趣的事件停蕉。

const salesOffices = {}

salesOffices.clientList = []

salesOffices.listen = function (key, fn) {
  if(!this.clientList[key]){
    this.clientList[key]=[]
  }
  this.clientList[key].push(fn)
}

salesOffices.trigger = function () {
  let key=Array.prototype.shift.call(arguments)
  let fns=this.clientList[key]
  if(!fns||fns.length===0){
    return
  }
  for (let i = 0, fn; fn = fns[i++];) {
    fn.apply(this, arguments)
  }
}

// 小明訂閱消息
salesOffices.listen('squareMeter88', function (price) {
  console.log('小明得到88平米的價格發(fā)布:' + price)
})

// 小紅訂閱消息
salesOffices.listen('squareMeter100', function (price) {
  console.log('小紅得到100平米的價格發(fā)布: ' + price)
})

// 小剛訂閱消息
salesOffices.listen('squareMeter88', function (price) {
  console.log('小剛得到88平米的價格發(fā)布:' + price)
})

salesOffices.trigger('squareMeter88',200000) // 小明得到88平米的價格發(fā)布:200000 小剛得到88平米的價格發(fā)布:200000
salesOffices.trigger('squareMeter100',300000) //小紅得到100平米的價格發(fā)布: 300000

但是,上面代碼存在幾個問題:

  • 沒有可擴(kuò)展性钙态,如果又有另外一個對象也需要這個模式慧起,豈不是要復(fù)制一遍一模一樣的代碼?
  • 沒有事件移除機(jī)制

第二版

相對第一版册倒,增加下面功能:

  • 給對象動態(tài)增加發(fā)布訂閱功能
  • 增加事件移除函數(shù)
  • 增加初始化綁定函數(shù)
const event = {
  clientList: [],
  listen(key, fn) {
    if (!this.clientList[key]) {
      this.clientList[key] = []
    }
    this.clientList[key].push(fn)

  },
  trigger() {
    let key = Array.prototype.shift.call(arguments)
    let fns = this.clientList[key]

    if (!fns || fns.length === 0) { // 沒有綁定對應(yīng)的消息
      return false
    }

    for (let i = 0, fn; fn = fns[i++];) {
      fn.apply(this, arguments)
    }
  },
  // 清除已有事件隊列蚓挤,重新綁定
  one(key, fn) {
    this.remove(key)
    this.listen(key, fn)
  },
  remove(key, fn) {
    let fns = this.clientList[key]

    if (!fns) { // 如果key沒有被人訂閱,則直接返回
      return
    }

    if (!fn) { // 如果沒有傳回調(diào)函數(shù)驻子,則表示取消key對應(yīng)的所有的訂閱
      fns.length = 0
    } else {
      for (let len = fns.length - 1; len >= 0; len--) {
        let _fn = fns[len]
        if (_fn === fn) {
          fns.splice(len, 1)
        }
      }
    }
  }
}

const installEvent = function (obj) {
  for (let i in event) {
    obj[i] = event[i]
  }
}

const salesOffices = {}
installEvent(salesOffices)

// 小明訂閱消息
salesOffices.listen('squareMeter88', ming = function (price) {
  console.log('小明得到88平米的價格發(fā)布:' + price)
})

// 小紅訂閱消息
salesOffices.listen('squareMeter100', hong = function (price) {
  console.log('小紅得到100平米的價格發(fā)布: ' + price)
})

// 小剛訂閱消息
salesOffices.listen('squareMeter88', gang = function (price) {
  console.log('小剛得到88平米的價格發(fā)布:' + price)
})

salesOffices.trigger('squareMeter88', 200000) // 小明得到88平米的價格發(fā)布:200000 小剛得到88平米的價格發(fā)布:200000
salesOffices.trigger('squareMeter100', 300000) //小紅得到100平米的價格發(fā)布: 300000

salesOffices.remove('squareMeter88', gang)
salesOffices.trigger('squareMeter88', 200000) // 小明得到88平米的價格發(fā)布:200000

第二版已經(jīng)比較的全面了灿意,但是還是存在一些不足:

  • 給每一個發(fā)布者都要添加listen方法和trigger方法,以及clientList列表崇呵,浪費資源脾歧。
  • 訂閱者需要知道發(fā)布者的名字,如果多個發(fā)布者發(fā)布同一個時間演熟,那么訂閱者需要訂閱多次鞭执,這顯然不是訂閱者希望看到的,因為他只關(guān)心事件本身芒粹,而非事件的發(fā)布者兄纺。

第三版

可以把事件發(fā)布完全委托給第三方管理,而非事件的本身發(fā)布者化漆。所以可以這樣寫:

const Event=(function(){
  let clientList=[],
  listen,
  trigger,
  remove

  listen=function(key,fn){
    if(!clientList[key]){
      clientList[key]=[]
    }
    clientList[key].push(fn)
  }

  trigger=function(){
    let key=Array.prototype.shift.call(arguments)
    let fns=clientList[key]
    if(!fns||fns.length===0){
      return 
    }
    for(let i=0,fn;fn=fns[i++];){
      fn.apply(this,arguments)
    }
  }

  remove=function(key,fn){
    let fns=clientList[key]
    if(!fns){
      return
    }
    if(!fn){
      fns.length=0
    }else{
      for(let len=fns.length-1;len>=0;len--){
        let _fn=fns[len]
        if(_fn===fn){
          fns.splice(len,1)
        }
      }
    }
  }

  return{
    listen,
    trigger,
    remove
  }

})()

// 小明訂閱消息
Event.listen('squareMeter88',ming=function (price){
  console.log('小明得到88平米的價格發(fā)布:'+price)
})

// 小剛訂閱消息
salesOffices.listen('squareMeter88', gang = function (price) {
  console.log('小剛得到88平米的價格發(fā)布:' + price)
})


Event.trigger('squareMeter88',2000000) // 小明得到88平米的價格發(fā)布:200000 小剛得到88平米的價
Event.remove('squareMeter88',ming)
Event.trigger('squareMeter88',2000000) // 小明得到88平米的價格發(fā)布:200000 

這樣就有了一個全局的事件管理對象估脆,發(fā)布者和訂閱者不用直接通信,而是通過這個第三方對象來進(jìn)行事件的發(fā)布訂閱座云。同時也真正實現(xiàn)了聚焦事件本身疙赠。

模式四-高級功能

  • 全局的發(fā)布—訂閱對象里只有一個clinetList來存放消息名和回調(diào)函數(shù),大家都通過它來訂閱和發(fā)布各種消息朦拖,久而久之圃阳,難免會出現(xiàn)事件名沖突的情況,所以我們還可以給Event對象提供創(chuàng)建命名空間的功能璧帝。
  • 另外捍岳,還有一點就是,我們想實現(xiàn)離線消息的功能睬隶。就是發(fā)布者可以現(xiàn)發(fā)布消息锣夹,然后等到訂閱者訂閱后,先完成發(fā)布者的離線消息確認(rèn)苏潜,然后再進(jìn)行常規(guī)的發(fā)布-訂閱操作银萍。
const Event = (function () {
  let _default = 'default'

  let Event = function () {
    // 這里的私有方法表示單純的事件方法,不包含命名空間和離線消息功能
    let _listen,
      _trigger,
      _remove,
      _shift = Array.prototype.shift,
      _unshift = Array.prototype.unshift,
      namespaceCache = {},
      _create,
      each = function (arr, fn) {
        let ret
        for (let i = 0, len = arr.length; i < len; i++) {
          let n = arr[i]
          ret = fn.call(n, i, n)
        }
        return ret
      }

    _listen = function (key, fn, cache) {
      if (!cache[key]) {
        cache[key] = []
      }
      cache[key].push(fn)
    }

    _remove = function (key, cache, fn) {
      if (cache[key]) {
        if (fn) {
          for (let len = cache[key].length - 1; len >= 0; len--) {
            if (cache[key][len] === fn) {
              cache[key].splice(len, 1)
            }
          }
        } else {
          cache[key].length = 0
        }
      }
    }

    _trigger = function () {
      let cache = _shift.call(arguments)
      let key = _shift.call(arguments)
      let args = arguments
      let stack = cache[key]
      let _self = this

      if (!stack || !stack.length) {
        return
      }

      return each(stack, function () {
        return this.apply(_self, args)
      })
    }

    _create = function (namespace = _default) {
      let cache = {}
      let offlineStack = []
      let ret = {
        // 這里的方法是對上面的原始方法進(jìn)行封裝恤左,混入離線消息和命名空間邏輯
        listen(key, fn, last) {
          _listen(key, fn, cache)
          if (offlineStack === null) {
            return
          }
          if (last === 'last') { // 表示彈出并執(zhí)行離線隊列的最后一個
            offlineStack.length && offlineStack.pop()()
          } else {
            each(offlineStack, function () {
              this()
            })
          }
          offlineStack = null
        },
        // 清除事件隊列的所有函數(shù)贴唇,然后調(diào)用listen函數(shù)
        // remove all + listen
        one(key, fn, last) {
          _remove(key, cache)
          this.listen(key, fn, last)
        },
        remove(key, fn) {
          _remove(key, cache, fn)
        },
        trigger() {
          let fn
          let args
          let _self = this
          _unshift.call(arguments, cache)
          args = arguments
          fn = function () {
            return _trigger.apply(_self, args)
          }

          if (offlineStack) {
            return offlineStack.push(fn)
          }
          return fn()
        }
      }
      // 外面一層贰锁,檢驗namespace參數(shù)是否能作為對象屬性,如果不能滤蝠,則不創(chuàng)建命名空間豌熄,直接返回一個新的對象,但這種場景沒有任何作用物咳,所以可以簡單理解為校驗namespace參數(shù)
      // 里面一層锣险,檢驗這個命名空間是否存在,如果存在則返回已存在的命名空間览闰,否則創(chuàng)建一個新的命名空間芯肤,并返回這個命名空間
      return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret
    }
    return {
      create: _create, // 創(chuàng)建命名空間對象以及這個對象的各種方法
      one: function (key, fn, last) {
        var event = this.create();
        event.one(key, fn, last);
      },
      remove: function (key, fn) {
        var event = this.create();
        event.remove(key, fn);
      },
      listen: function (key, fn, last) {
        var event = this.create();
        event.listen(key, fn, last);
      },
      trigger: function () {
        var event = this.create();
        event.trigger.apply(this, arguments);
      }
    }
  }
  return Event()
})()

這里可以有幾種用法:

  1. 使用者可以自己創(chuàng)建命名空間,然后在自己的命名空間里管理消息压鉴。
Event.create( 'namespace1' ).listen( 'click', function( a ){
    console.log( a );    // 輸出:1
});

Event.create( 'namespace1' ).trigger( 'click', 1 );
  1. 使用者也可以不用自己創(chuàng)建空間崖咨,直接使用默認(rèn)的命名空間。
Event.listen( 'click', function( a ){
    console.log( a );       // 輸出:1
});
Event.trigger( 'click', 1 );
  1. 使用者可以先發(fā)布離線消息油吭,然后在訂閱者訂閱的時候自動處理击蹲。
Event.trigger( 'click', 1 );

Event.listen( 'click', function( a ){
    console.log( a );       
}); // 1

Event.trigger( 'click', 2 ); // 2

發(fā)布—訂閱模式的優(yōu)點非常明顯,一為時間上的解耦婉宰,二為對象之間的解耦歌豺。它的應(yīng)用非常廣泛,既可以用在異步編程中心包,也可以幫助我們完成更松耦合的代碼編寫类咧。發(fā)布—訂閱模式還可以用來幫助實現(xiàn)一些別的設(shè)計模式,比如中介者模式蟹腾。 從架構(gòu)上來看痕惋,無論是MVC還是MVVM,都少不了發(fā)布—訂閱模式的參與娃殖,而且JavaScript本身也是一門基于事件驅(qū)動的語言值戳。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市珊随,隨后出現(xiàn)的幾起案子述寡,更是在濱河造成了極大的恐慌柿隙,老刑警劉巖叶洞,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異禀崖,居然都是意外死亡衩辟,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門波附,熙熙樓的掌柜王于貴愁眉苦臉地迎上來艺晴,“玉大人昼钻,你說我怎么就攤上這事》饽” “怎么了然评?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長狈究。 經(jīng)常有香客問我碗淌,道長,這世上最難降的妖魔是什么抖锥? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任亿眠,我火速辦了婚禮,結(jié)果婚禮上磅废,老公的妹妹穿的比我還像新娘纳像。我一直安慰自己,他們只是感情好拯勉,可當(dāng)我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布竟趾。 她就那樣靜靜地躺著,像睡著了一般宫峦。 火紅的嫁衣襯著肌膚如雪潭兽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天斗遏,我揣著相機(jī)與錄音山卦,去河邊找鬼。 笑死诵次,一個胖子當(dāng)著我的面吹牛账蓉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逾一,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼铸本,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了遵堵?” 一聲冷哼從身側(cè)響起箱玷,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎陌宿,沒想到半個月后锡足,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡壳坪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年舶得,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爽蝴。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡沐批,死狀恐怖纫骑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情九孩,我是刑警寧澤先馆,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站躺彬,受9級特大地震影響磨隘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜顾患,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一番捂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧江解,春花似錦设预、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至桨螺,卻和暖如春宾符,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背灭翔。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工魏烫, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肝箱。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓哄褒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親煌张。 傳聞我的和親對象是個殘疾皇子呐赡,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內(nèi)容