js设计模式-发布订阅模式

192 阅读2分钟

定义

发布—订阅模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般用事件模型来替代传统的发布—订阅模式。

代码实现

  • 通过全局对象来把订阅者和发布者联系起来
// index.vue
<template>
<button @click="addVal">增加</button>
<div>{{val}}</div>
<button @click="remove">移除绑定</button>
</template>

<script lang="ts">
import event from '@/utils/event'
import { defineComponent, ref } from 'vue'
export default defineComponent({
  setup() {
    const val = ref(0)
    const fn = function(price = 1) {
      val.value += price
    }
    event.listen('addval', fn)
    return {
      val,
      fn
    }
  },
  methods: {
    addVal() {
      event.trigger('addval',9)
    },
    remove() {
      event.remove('addval', this.fn)
    }
  }
})
</script>
// event.js
class Event {
  constructor() {
    this.clientList = [];
  }

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

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

  remove(key, fn) {
    const fns = this.clientList[key]
    if (!fns) return false
    if (fn) {
      fns && (fns.length = 0)
    } else {
      for (let i = 0; i < fns.length; i++) {
        if (fns[i] === fn) fns.splice(i, 1)
      }
    }
  }
}

export default new Event()

需求升级

  • 增加命名空间
  • 增加先发布后订阅的功能 😔 *以下代码功能不太完善还需优化,技术不精,暂未成功 *
// index.vue
<template>
<button @click="eventer">先发布后订阅</button>
</template>

<script lang="ts">
import eventer from '@/utils/eventer'
import { defineComponent, ref } from 'vue'
export default defineComponent({
  methods: {
    eventer() {
      eventer.trigger('click', 1)
      eventer.listen('click', function(a) {
        console.log(a)
      })
      // 多次添加订阅,先发布后订阅的功能有问题,第一次以外的订阅都不会生效
      eventer.listen('click', function(a) {
        console.log(a + 1)
      })
      eventer.create('namespace1').listen('click', function(a) {
        console.log(a, 'namespace1')
      })
      eventer.create('namespace1').trigger('click', 1)
      eventer.create('namespace2').listen('click', function(a) {
        console.log(a, 'namespace2')
      })
      eventer.create('namespace2').trigger('click', 2)
    }
  }
})
</script>
// eventer.js
export default Event = (function() {
  var Event,
  _default = 'default';
  Event = function() {
    var _listen,
    _trigger,
    _remove,
    _shift = Array.prototype.shift,
    _unshift = Array.prototype.unshift,
    namespaceCache = {}, // 命名空间缓存对象
    _create,
    each = function(ary, fn) {
      var ret;
      for (let i = 0; i < ary.length; i++) {
        const n = ary[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 i = 0; i < cache[key].length; i++) {
            if (cache[key][i] === fn) cache[key].splice(i, 1)
          }
        } else {
          cache[key] = []
        }
      }
    }
    _trigger = function() {
      var cache = _shift.call(arguments),
      key = _shift.call(arguments),
      args = arguments,
      _self = this,
      stack = cache[key]
      if (!stack || !stack.length) return
      return each(stack, function() {
        return this.apply(_self, args)
      })
    }
    _create = function(namespace) {
      var namespace = namespace || _default,
      cache = {}, // 单事件缓存对象
      offlineStack = [], // 单个event离线事件队列
      ret = {
        listen: function(key, fn, last) {
          _listen(key, fn, cache) // 添加订阅
          if (offlineStack === null) return
          if (last === 'last') {
            offlineStack.length && offlineStack.pop()()
          } else {
            each(offlineStack, function() {
              this()
            })
          }
          offlineStack = null
        },
        one: function(key, fn, last) {
          _remove(key, cache)
          this.listen(key, fn, last)
        },
        remove: function(key, fn) {
          _remove(key, cache, fn)
        },
        trigger: function() {
          var fn,
          args,
          _self = this;
          _unshift.call(arguments, cache)
          args = arguments;
          fn = function() {
            each(offlineStack, function(n, i) {
              if (n === fn) offlineStack.splice(i, 1)
            })
            return _trigger.apply(_self, args)
          }
          if (offlineStack) return offlineStack.push(fn)
          return fn()
        }
      }
      return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret
    }
    // 类似于代理模式,返回与真正单个event相同的接口
    return {
      create: _create,
      one: function(key, fn, last) {
        const event = this.create()
        event.one(key, fn, last)
      },
      remove: function(key, fn) {
        const event = this.create()
        event.remove(key, fn)
      },
      listen: function(key, fn, last) {
        const event = this.create()
        event.listen(key, fn, last)
      },
      trigger: function() {
        const event = this.create()
        event.trigger.apply(this, arguments)
      }
    }
  }()
  return Event
})()

优缺点

  • 优点:发布订阅模式可以做到时间上的解耦以及对象之间的解耦,这样即使在异步编程中我们也可以完成更松耦合的代码编写
  • 缺点:创建订阅者需要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息到最后也未发生,但这个订阅者始终存在于内存中;另外过度使用时,对象与对象之间的必要联系会被深埋在背后,导致程序难以跟踪维护和理解。