JavaScript设计模式———订阅-发布者模式

这里写自定义目录标题

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

实现一个发布订阅模式

var Event = function() {
  this.obj = {}
}

Event.prototype.on = function(eventType, fn) {
  if (!this.obj[eventType]) {
    this.obj[eventType] = []
  }
  this.obj[eventType].push(fn)
}

Event.prototype.emit = function() {
  var eventType = Array.prototype.shift.call(arguments)
  var arr = this.obj[eventType]
  for (let i = 0; i < arr.length; i++) {
    arr[i].apply(arr[i], arguments)
  }
}

var ev = new Event()

ev.on('click', function(a) { // 订阅函数
  console.log(a) // 1
})

ev.emit('click', 1)          // 发布函数

发布-订阅模式的通用实现
为对象添加订阅发布模式

var event = {
    clientList: [],
    listen: function(key, fn) {
        if (!this.clientList[key]) {
            this.clientList[key] = [];
        }
        this.clientList[key].push(fn); // 订阅的消息添加进缓存列表
    },
    trigger: function() {
        var key = Array.prototype.shift.call(arguments), // (1);
            fns = this.clientList[key];
        if (!fns || fns.length === 0) { // 如果没有绑定对应的消息
            return false;
        }
        for (var i = 0, fn; fn = fns[i++];) {
            fn.apply(this, arguments); // (2) // arguments 是 trigger 时带上的参数
        }
    }
};

// 为对象添加发布-订阅功能
var installEvent = function(obj) {
    for (var i in event) {
        obj[i] = event[i];
    }
};

var objTest = {}
// 为对象objTest添加发布订阅功能
installEvent(objTest)

全局的发布- 订阅对象
为每个对象添加订阅和发布的功能,这里还存在两个小问题

  • 每个发布者对象都添加了 listen 和 trigger 方法,以及一个缓存列表 clientList,这其实是一种资源浪费。
  • 发布对象和订阅对象有一定耦合性,订阅对象至少需要知道发布对象。

利用一个全局的发布订阅对象,解决以上问题:

var Event = (function() {
    var clientList = {},
        listen,
        trigger,
        remove;
    listen = function(key, fn) {
        if (!clientList[key]) {
            clientList[key] = [];
        }
        clientList[key].push(fn);
    };
    trigger = function() {
        var key = Array.prototype.shift.call(arguments),
            fns = clientList[key];
        if (!fns || fns.length === 0) {
            return false;
        }
        for (var i = 0, fn; fn = fns[i++];) {
            fn.apply(this, arguments);
        }
    };
    remove = function(key, fn) {
        var fns = clientList[key];
        if (!fns) {
            return false;
        }
        if (!fn) {
            fns && (fns.length = 0);
        } else {
            for (var l = fns.length - 1; l >= 0; l--) {
                var _fn = fns[l];
                if (_fn === fn) {
                    fns.splice(l, 1);
                }
            }
        }
    };
    return {
        listen: listen,
        trigger: trigger,
        remove: remove
    }
})();
Event.listen('squareMeter88', function(price) { // 小红订阅消息
    console.log('价格= ' + price); // 输出: '价格=2000000'
});
Event.trigger('squareMeter88', 2000000); // 售楼处发布消息

必须先订阅再发布吗

考虑以下场景:

$.ajax('', () => {
  // 异步订阅函数逻辑
})
// 在其他地方执行发布函数, 此时并不能保证执行发布函数的时候, 订阅函数已经执行
//我们需要实现这样的逻辑:
var ev = new Event()
ev.emit('click', 1)
ev.on('click', function(a) {
  console.log(a) // 1
})

我们需要实现这样的逻辑:

我们所了解到的发布—订阅模式,都是订阅者必须先订阅一个消息,随后才能接收到发布者发布的消息。如果把顺序反过来,发布者先发布一条消息,而在此之前并没有对象来订阅它,这条消息无疑将消失在宇宙中。

为了满足这个需求,我们要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。当然离线事件的生命周期只有一次,就像 QQ 的未读消息只会被重新阅读一次,所以刚才的操作我们只能进行一次。

var Event = function() {
  this.obj = {}
  this.cacheList = []
}

Event.prototype.on = function(eventType, fn) {
  if (!this.obj[eventType]) {
    this.obj[eventType] = []
  }
  this.obj[eventType].push(fn)

  for (let i = 0; i < this.cacheList.length; i++) {
    this.cacheList[i]()
  }
}

Event.prototype.emit = function() {
  const arg = arguments
  const that = this
  function cache() {
    var eventType = Array.prototype.shift.call(arg)
    var arr = that.obj[eventType]
    for (let i = 0; i < arr.length; i++) {
      arr[i].apply(arr[i], arg)
    }
  }
  this.cacheList.push(cache)
}
// 以上代码实现思路就是把原本在 emit 里触发的函数存到 cacheList, 再转交到 on 中触发。从而实现了发布函数先于订阅函数执行。

总结

发布—订阅模式的优点非常明显

  • 时间上的解耦
  • 对象之间的解耦
    它的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。

缺点:

  • 创建订阅者本身要消耗一定的时间和内存:而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。
  • 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联
    系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一
    起的时候,要跟踪一个 bug 不是件轻松的事情。
posted @ 2022-07-07 03:57  CD、小月  阅读(28)  评论(0编辑  收藏  举报  来源