Javascript设计模式之发布-订阅模式

定义

发布-订阅模式是观察者模式的一种,它定义一个对象和多个对象之间的依赖关系,当对象的状态发生改变时,全部依赖它的对象都会收到通知。在JavaScript中,咱们通常使用事件模型来代替传统的发布-订阅模式。javascript

DOM事件

在DOM编程中,咱们常常会监听一些DOM事件,至关于订阅这个事件,而后用户触发这个事件,咱们就能在回调中作一些操做。例如:java

document.body.addEventListener('click', function (e) {
  console.log('clicked')
}, false)

document.body.click()   // 模拟用户点击
复制代码

咱们不知道何时用户会触发点击事件,咱们只须要订阅它,而后等到事件触发,就能收到通知。ajax

实现发布-订阅模式

简单的版本编程

const event = {
  clientList: [],
  listen: function (fn) {
    this.clientList.push(fn)
  },
  trigger: function () {
    for (let i = 0, len = this.clientList.length; i < len; i++) {
      const fn = this.clientList[i]
      fn.apply(this, arguments)
    }
  }
}
复制代码

可是这个简单的版本有一些问题,若是订阅者A和订阅B它们对发布者的感兴趣的事件不同,可是不管发布者触发什么事件,A和B都会收到通知。因此咱们添加eventChannel,对不一样的订阅者进行分类:api

const event = {
  clientList: {},
  listen: function (channel, fn) {
    if (!this.clientList[channel]) {
      this.clientList[channel] = []
    }
    this.clientList[channel].push(fn)
  },
  trigger: function () {
    const channel = arguments[0]
    const clientList = this.clientList[channel] || []
    if (clientList.length === 0) {
      return
    }
    for (let i = 0, len = clientList.length; i < len; i++) {
      const fn = clientList[i]
      fn.apply(this, arguments)
    }
  }
}
复制代码

取消订阅
若是某个订阅者对以前订阅的channel不感兴趣了,还须要提供一个方法取消订阅。app

event.remove = function (channel, fn) {
  let clientList = this.clientList[channel] || []
  if (clientList.length === 0) {
    return
  }
  if (!fn) {
    clientList = []
  } else {
    for (let i = 0, len = clientList.length; i < len; i++) {
      const _fn = clientList[i]
      if (_fn === fn) {
        clientList.splice(i, 1)
      }
    }
  }
}
复制代码

应用

假设咱们接到一个需求,用户登陆以后,须要更新网站的header头部、nav导航、购物车、消息列表等模块的用户信息。更新上面所列举的模块的前提条件就是经过ajax异步获取用户的登陆信息,由于ajax是异步的,何时返回登陆信息咱们是不知道的,最经常使用的作法是经过回调来解决,因而有以下代码:异步

login.success(function(data) {
  header.setAvatar(data.avatar)
  nav.setAvatar(data.avatar)
  message.refresh()
  cart.refresh()
})
复制代码

上面代码的问题是,若是我负责的是登陆模块,上面的其它header、nav、购物车模块是其它同事负责的,我必须还得了解其它模块的api,好比header模块的setAvatar等等,这种耦合性使程序变得僵硬。若是哪天要重构其它模块的代码,那api的名字不能随便修改,模块名也不能随意修改。若是项目又新增了一个地址模块,这个模块在用户登陆以后也须要刷新,可是地址模块是其它同事负责的,那这个同事还得找到你,叫你在登陆成功以后刷新地址列表。因而又增长了代码:异步编程

login.success(function(data) {
  header.setAvatar(data.avatar)
  nav.setAvatar(data.avatar)
  message.refresh()
  cart.refresh()
  address.refresh()
})
复制代码

这种修改会让人疲倦,让开发人员失去耐心。这个时候,就须要发布-订阅模式出场,重构代码。
使用发布-订阅模式重构的思路是,在用户登陆成功以后,发布一个登陆成功的消息,须要刷新用户数据的模块就能够订阅这个事件,而后去调用本身的方法更新数据或者作其它的业务处理,从而解耦了登陆模块和其它模块,登陆模块不用关心其它模块须要作什么,也不用关心各模块的内部细节。重构的代码以下:网站

$.ajax('http://xxx.com?login', function (data) {
  login.trigger('loginSuccess', data)
})
复制代码

各模块监听登陆成功的消息:ui

const header = (function () {
  login.listen('loginSuccess', function (data) {
    header.setAvatar(data.avatar)
  })
  return {
    setAvatar: function (data) {
      console.log('设置header模块的头像')
    }
  }
})()
复制代码

这样就算其它模块修改方法的名字或者哪一天又新增了模块须要更新用户数据,登陆模块不须要关心,各个模块本身处理就好了。

总结

经过上面的应用咱们得出发布-订阅模式的优势,一为时间上的解耦,订阅者不须要关心何时发布者会发布事件;二为对象之间的解耦,上面的登陆功能完美验证了这个点。发布-订阅模式应用很是普遍,既能够应用异步编程,也能够帮助咱们完成更松耦合的代码编写。 发布-订阅模式也不是完美的,它也有本身的缺点。首先,建立订阅者就要消耗时间和内存,当你订阅一个消息,若是这个消息始终没有发生,那这个订阅者也会一直在内存中。它虽然弱化了对象之间的联系,但若是过分使用,对象和对象之间的必要联系也会深埋在背后,致使程序难以跟踪维护和理解。特别是若是多个发布者和订阅者嵌套在一块儿的时候,跟踪bug也变得更加困难。

相关文章
相关标签/搜索