美文网首页
JavaScript设计模式之发布-订阅模式

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

作者: 李友胜 | 来源:发表于2019-08-28 23:18 被阅读0次

    发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知

    1. 现实中的发布-订阅模式

    小明最近看上一套房子,到了售楼处之后才被告知,楼盘房子早已售空。好在售楼处告诉小明,不久后还有一些尾盘推出,但到底是什么时候,目前还不知道。
    于是小明记下了售楼处的电话,每天都会打电话询问有没有开盘。同样,除了小明,还有小红、小强也每天都会像售楼处打电话。一个星期后,售楼处电话已厌倦了每天的电话回答相同的内容。
    当然,现实中显然不是这样的,实际上是:售楼处会把意向的客户的手机号码留在售楼处,也就是小红,小明,小强的电话,当开盘时,会依次发送短信来通知他们。

    2. 发布-订阅模式的作用

    在刚刚的例子中,发短信通知就是一个典型的发布-订阅模式。小明,小红他们都是订阅者,他们订阅了房子开盘的消息。售楼处作为发布者,会在合适的时候依次发送给所有订阅者发送消息。
    优点:

    • 购房者不用天天打电话给售楼处,在合适的时间,售楼处会作为发布者通知给所有的订阅者
    • 购房者和售楼处之前不再强耦合的在一起,当有新的购房者出现时,只需要留下手机号码,售楼处不需要关系购房者的任何情况,只需要知道谁订阅了购房消息即可。
      第一条说明发布订阅模式可以广泛引用于异步编程中,这是一种替代传递回调函数的方案。比如,订阅ajax请求的success,error事件。或者如果想在动画的每一帧完成之后做一些事情,那我们可以订阅一个事件,然后在动画的每一帧完成之后去发布这个事件。在异步变成中使用发布-订阅模式,我们就无序过多关注对象在异步运行中的内部状态,而只需要订阅感兴趣的事件发生点。
      第二条说明发布-订阅模式可以取代对象之间硬编码的通知机制,一个对象不再显试的嗲用另外一个对象的某个接口。发布-订阅模式让两个对象松耦合的联系在一起,虽然不太清除彼此的细节,但不影响他们互相通信。当有新的订阅者出现,发布者的代码不需要做任何改变,同样发布者需要改变,订阅者也不会影响。只要之前约定的事件名没有变化。
    3.DOM事件

    实际上,只要我们曾经在DOM节点上绑定过事件函数,那我们就曾经使用过发布-订阅模式。

    document.body.addEventListener('click', function(){
        alert(1);
    }, false);
    document.body.click(); // 模拟用户点击
    

    在这里我们需要监控用户点击body的动作,但我们没法预知用户点击的时间。所以我们订阅了document.body上的click事件,当body节点被点击时,body节点便会像订阅者发布这个消息。

    4.自定义事件

    如何一步步实现发布-订阅模式

    • 首先制定好谁是发布者
    • 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者。
    • 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数。
      另外,可以往回调函数里填入一些参数,订阅者可以接受这些参数。这是很有必要的,比如售楼处可以在发给订阅者消息里面添加房子面积,单价等信息,订阅者可以接受到消息做各自的处理
     var salesOffices = {}  // 定义售楼处
    
        salesOffices.clientList = []  // 缓存列表,存放订阅者的回调函数
    
        salesOffices.listen = function (fn) {  // 增加订阅者
            this.clientList.push(fn) // 订阅的消息添加进缓存列表
        }
    
        salesOffices.trigger = function () {  // 发布消息
            for (var i = 0; i < this.clientList.length; i++) {
                var fn = this.clientList[i];
                fn.apply(this, arguments) // arguments 是发布消息时带上的参数
            }
        }
    
        // 接下来进行简单的测试
    
        salesOffices.listen(function (price, squareMeter) { // 小明
            console.log('价格' + price)
            console.log('面积' + squareMeter)
        })
    
        salesOffices.listen(function (price, squareMeter) { // 小红
            console.log('价格' + price)
            console.log('面积' + squareMeter)
        })
    
        salesOffices.trigger(200000, 88)
        salesOffices.trigger(300000, 99)
    

    至此,我们实现了一个最简单的发布-订阅模式,但是这里有一些问题,我们看到订阅者接收到了发布者的每个消息,虽然小明只想买88平方米的房子,但是同时也接受到了99平方米的信息,所以我们需要标识一个key,让订阅者制定粤自己感兴趣的消息,改写后代码如下:

    var salesOffices = {}  // 定义售楼处
    
        salesOffices.clientList = {}  // 缓存列表,存放订阅者的回调函数
    
        salesOffices.listen = function (key, fn) {  // 增加订阅者
            if (!this.clientList[key]){  // 如果还没有订阅过此类的消息,给该类消息创建一个缓存列表
                this.clientList[key] = []
            }
            this.clientList[key].push(fn) // 订阅的消息添加到缓存列表
        }
    
        salesOffices.trigger = function () {  // 发布消息
    
            var key = Array.prototype.shift.call(arguments) // 取出消息类型,也就是key
            var fns = this.clientList[key] // 取出该类型所有的回调函数集合
    
            if (!fns || fns.length === 0) return false; // 没有订阅过该消息就返回
    
            for (var i = 0; i < fns.length; i++) {
                var fn = fns[i]
                fn.apply(this, arguments)
            }
        }
    
    
        salesOffices.listen('m88', function (price) {
            console.log(price);
        })
    
        salesOffices.listen('m99', function (price) {
            console.log(price);
        })
    
        salesOffices.trigger('m88', 200000)
        salesOffices.trigger('m99', 300000)
    

    很明显,订阅者可以只订阅自己感兴趣的事件了。

    5.发布-订阅模式的通用实现

    小明又想去其他售楼处买房,那么就需要重新写一个salesOffices,所以需要做如下改进,把发布订阅的功能提取出来,单独放在一个对象内:

    
        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) // 取出消息类型,也就是key
                var fns = this.clientList[key] // 取出该类型所有的回调函数集合
    
                if (!fns || fns.length === 0) return false; // 没有订阅过该消息就返回
    
                for (var i = 0; i < fns.length; i++) {
                    var fn = fns[i]
                    fn.apply(this, arguments)
                }
            }
        }
    
        // 定义一个installEvent函数,这个函数可以给所有对象都动态安装发布-订阅功能:
        var installEvent = function (obj) {
            for (var i in event) {
                obj[i] = event[i]
            }
        }
    
        var salesOffices = {}
    
        installEvent(salesOffices)
    
        salesOffices.listen('ss88', function (price) {
            console.log(price);
        })
    
        salesOffices.listen('ss99', function (price) {
            console.log(price)
        })
    
        salesOffices.listen('ss88', 200000)
        salesOffices.listen('ss99', 300000)
    
    6. 取消订阅的事件

    有时候我们有这种需求,小明突然不想买房了,为了避免售楼处推送来的信息,可以取消之前订阅的事件

     event.remove = function (key, fn) {
            var fns = this.clientList[key]
    
            if (!fns) return false; // 如果key对应得消息没有被人订阅,则返回
            if (!fn){
                fns && (fns.length = 0) // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
            }else {
                for (var i = fns.length - 1; i > 0; i--) { // 反向遍历订阅的回调函数列表
                    var _fn = fns[i]
                    if (_fn === fn){
                        fns.splice(i, 1) // 删除对应得回调函数
                    }
                }
            }
    
        }
    
        var salesOffices = {}
    
        installEvent(salesOffices)
    
        salesOffices.listen('ss88', fn1 = function (price) {
            console.log(price);
        })
    
        salesOffices.listen('ss99', fn2 = function (price) {
            console.log(price)
        })
    
        salesOffices.remove('ss88', fn1)
        salesOffices.trigger('ss88', 200000)
    
    7.真实的例子--网站登录

    加入我们正在开发一个网站商场,网站里面有header,nav,购物车等各模块。这几个模块的渲染有一个共同的前提条件,就是必须先用ajax异步请求获取用户的信息。
    至于ajax请求什么时候能成功返回用户信息,这点我们没法确定。更重要的一点是,我们不知道除了购物车,nav,header之外,还有哪些地方需要用到用户信息。如果他们和用户之间信息产生了强耦合,比如下面这样的形式:

    login.succ(function(data){
        header.setAcatar(data.avatar) // 设置header模块的头像
        nav.setAvatar(data.avatar) // 设置导航模块的头像
        message.refresh() // 消息列表刷新
        cart.refresh() // 购物车刷新
    })
    
    

    现在登录模块是我们负责编写的,我们必须还了解header模块设置头像的方法叫setAvatar,购物车刷新的方法叫refresh.等等各种方法,如果后面需要加上收货地址的模块

    login.succ(function(data){
        header.setAcatar(data.avatar) // 设置header模块的头像
        nav.setAvatar(data.avatar) // 设置导航模块的头像
        message.refresh() // 消息列表刷新
        cart.refresh() // 购物车刷新
        adress.refresh()  // 增加
    })
    
    

    这时候,又要重构代码。
    当我们用发布订阅模式重写后,对用户感兴趣的业务模块将自定订阅登录成功的消息事件。登录成功后,登录模块只需要发布登录信息,而业务方接受消息之后,可以自己做各自的业务处理。

    $.ajax('http://xxx.xx.com/login',function(data){
        login.trigger('loginsuccess', data)
    })
    

    各模块监听登录成功的消息

    var header = (function () {
            login.listen('loginsuccess', function (data) {
                header.setAvator(data.avator)
            });
            return {
                setAvator: function(data) {
                    console.log('设置header的头像')
                }
            }
        })()
    

    这时候,如果添加一个刷新收货地址的行为,就可以让收货地址模块开发人员去订阅登录成功这个事件

        var adderss = (function () {
            login.listen('loginsuccess', function (data) {
                adderss.refresh(data)
            });
            return {
                refresh: function (data) {
                    console.log('刷新地址')
                }
            }
        })()
    
    8. 全局的发布-订阅对象

    用一个全局的Event对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不需要消息会推送给订阅者,Event作为一个中介者的角色来吧发布者和订阅者联系到一起,代码如下:

    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) // 取出消息类型,也就是key
                var fns = clientList[key] // 取出该类型所有的回调函数集合
    
                if (!fns || fns.length === 0) return false; // 没有订阅过该消息就返回
    
                for (var i = 0; i < fns.length; i++) {
                    var fn = fns[i]
                    fn.apply(this, arguments)
                }
            }
    
            remove = function (key, fn) {
                var fns = clientList[key]
    
                if (!fns) return false; // 如果key对应得消息没有被人订阅,则返回
                if (!fn){
                    fns && (fns.length = 0) // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
                }else {
                    for (var i = fns.length - 1; i > 0; i--) { // 反向遍历订阅的回调函数列表
                        var _fn = fns[i]
                        if (_fn === fn){
                            fns.splice(i, 1) // 删除对应得回调函数
                        }
                    }
                }
            }
    
            return {
                listen: listen,
                trigger: trigger,
                remove: remove
            }
    
        })()
    
    
        Event.listen('ss88', function (data) {
            console.log(data);
        })
    
        Event.trigger('ss88', 222)
    

    相关文章

      网友评论

          本文标题:JavaScript设计模式之发布-订阅模式

          本文链接:https://www.haomeiwen.com/subject/rulvectx.html