资讯专栏INFORMATION COLUMN

Javascript(七)发布-订阅模式

megatron / 813人阅读

摘要:发布订阅的作用发布订阅模式可以广泛应用与异步编程中,这是一种替代传统回调函数的方案。发布订阅模式让两个对象松散耦合地联系在一起,虽然不清楚彼此的细节,但不影响它们之间的通信。

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

发布-订阅模式可以广泛应用与异步编程中,这是一种替代传统回调函数的方案。

比如订阅ajax的succ,error等事件,只需订阅事件发生点,而无需关注异步运行期间状态

发布-订阅可以取代对象之间硬编码的通信机制,一个对象不再显式地调用另外一个对象的某个接口。

发布-订阅模式让两个对象松散耦合地联系在一起,虽然不清楚彼此的细节,但不影响它们之间的通信。

当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者改变时,也不会影响到之前的订阅者。

发布-订阅模式的通用实现
var event = {
    //缓存列表,存放订阅者的回调函数
    clientList: [],
    //增加订阅者, key:事件名,fn:回调函数
    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)
            // 取出消息类型对应的回调函数集合
            fns = this.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 = this.clientList[key]
        // 如果对应的消息没人订阅,直接返回
        if(!fns) {    
            return false
        }
        // 如果没有传入回调函数,表示取消key对应消息的所有订阅
        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)
                }
            }
        }
    }
}

var salesOffices = {}
// 给对象安装发布-订阅功能
var installEvent = function(obj){
    for(var i in event){
        obj[i] = event[i]
    }
}
installEvent(salesOffices)


salesOffices.listen("squareMeter88", fn1 = function(price){
    console.log("价格1:"+price)
})

salesOffices.listen("squareMeter87", fn2 = function(price){
    console.log("价格2:"+price)
})    

salesOffices.trigger("squareMeter88", 20000) // 价格1: 20000

salesOffices.remove("squareMeter88")

salesOffices.trigger("squareMeter88", fn1)  // 打印结果为空

其实在这个发布-订阅模式中还存在两个问题:

给对象多带带添加其实是一种资源浪费

发布者与订阅者存在一定耦合性,至少要知道salesOffices这个对象

所以接下来实现一个全局的发布-订阅对象

var Event = (function(){
    var clientList = {},
        listten,
        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,
        trigger,
        remove
    }
})()
Event.listen("squareMeter88", fn1 = function(price){ // 订阅消息
    console.log("价格1:"+price)
})
Event.trigger("squareMeter88", 20000) // 价格1: 20000
必须先订阅在发布吗

我们所了解的订阅发布都是订阅者必须先订阅一个消息,随后才能接受到发布者发布的消息。如果顺序反过来,发布者先发布一条消息,而此时没有订阅者,无疑消息会消失。

探讨下这种需求

在某些情况下,我们需要先讲这条消息保存下来,等到有对象订阅它的时候,再重新把消息发布给订阅者,如果QQ中的离线消息,离线消息先被保存在服务器中,接收人下次登陆之后,可以从新接收到这条消息。

再假如一个商城系统中,获取到用户信息后才能渲染导航模块,无疑这是一个异步的过程,不能保证ajax何时返回。所以我们需要我们发布订阅对象拥有先发布后订阅的能力。

如何实现呢

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

命名冲突

全局的订阅-发布对象里只有一个clientList来存放消息名和回调函数,我们都通过它来订阅和发布各种消息,久而久之,难免会出现事件名冲突的情况,所以需要给Event对象提供创建命名空间的功能。

具体实现
var Event = (function() {

    var Event,
        _default = "default";

    Event = function() {
        var _listen,
            _trigger,
            _remove,
            _shift = [].shift,
            _unshift = [].unshift,
            namespaceCache = {},
            _create,
            each = function(arr, fn) {
                var ret;
                for (var i = 0, l = arr.length; i < l; i++) {
                    var 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 (var i = cache[key].length - 1; i >= 0; i--) {
                        if (cache[key][i] === fn) { 
                            // 删除订阅回调函数
                            cache[key].splice(i, 1);
                        }
                    }
                }
                else {
                    cache[key] = [];
                }
            }
        };

        _trigger = function() {
            var cache = _shift.call(arguments);
            var key = _shift.call(arguments);
            var args = arguments;
            var _this = this;
            var stack = cache[key];

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

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

        _create = function(namespace) {
            namespace = namespace || _default;
            var cache = {};
            var offlineStack = []; //离线事件
            var 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,
                        _this = this;

                    _unshift.call(arguments, cache);
                    args = arguments;

                    fn = function() {
                        return _trigger.apply(_this, args);
                    };

                    if (offlineStack) {
                        return offlineStack.push(fn);
                    }

                    return fn();
                }
            };

            return namespace ?
                (namespaceCache[namespace] ?
                    namespaceCache[namespace] : namespaceCache[namespace] = ret)
                : ret;
        };

        return {
            create: _create,
            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;
}());

var fn1 = function(price) {
    console.log(price);
};

// 实例
Event.listen("squareMeter88", fn1);
Event.remove("squareMeter88", fn1);

Event.listen("squareMeter88", function(price) {
    console.log("fn2: " + price);
});


Event.trigger("squareMeter88", 20000);   // fn2: 20000
总结 优点

时间与对象之间的解耦,应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写,从架构上看,无论MVC还是MVVM都少不了发布-订阅的参与

缺点

创建订阅者本身要消耗一定时间和内存,无论消息是否发生,一直存在内存中。虽然发布-订阅模式虽然可以弱化对象之间的联系,但是过度使用,对象之间的必然联系会被深埋,特别是多个订阅者和发布者嵌套会造成难以追踪的bug。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108683.html

相关文章

  • JavaScript 模式》知识点小抄本(下)

    摘要:缺点不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。预防低水平人员带来的风险。开闭原则,高拓展性。这里的订阅者称为观察者,而被观察者称为发布者,当一个事件发生,发布者会发布通知所有订阅者,并常常以事件对象形式传递消息。 介绍 最近开始给自己每周订个学习任务,学习结果反馈为一篇文章的输出,做好学习记录。 这一周(02.25-03.03)我定的目标是《JavaScript 模式》...

    xiguadada 评论0 收藏0
  • 【CuteJavaScript】Angular6入门项目(3.编写服务和引入RxJS)

    摘要:发布通过回调方法向发布事件。观察者一个回调函数的集合,它知道如何去监听由提供的值。 本文目录 一、项目起步 二、编写路由组件 三、编写页面组件 1.编写单一组件 2.模拟数据 3.编写主从组件 四、编写服务 1.为什么需要服务 2.编写服务 五、引入RxJS 1.关于RxJS 2.引入RxJS 3.改造数据获取方式 六、改造组件 1.添...

    RebeccaZhong 评论0 收藏0
  • JavaScript设计模式发布-订阅模式(观察者模式)-Part1

    摘要:设计模式与开发实践读书笔记。发布订阅模式又叫观察者模式,它定义了对象之间的一种一对多的依赖关系。附设计模式之发布订阅模式观察者模式数据结构和算法系列栈队列优先队列循环队列设计模式系列设计模式之策略模式 《JavaScript设计模式与开发实践》读书笔记。 发布-订阅模式又叫观察者模式,它定义了对象之间的一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖它的对象都将得到通知。 例...

    muzhuyu 评论0 收藏0
  • JavaScript 发布-订阅模式

    摘要:发布订阅模式订阅者把自己想订阅的事件注册到调度中心,当发布者发布该事件到调度中心,也就是该事件触发时,由调度中心统一调度订阅者注册到调度中心的处理代码。 发布-订阅模式,看似陌生,其实不然。工作中经常会用到,例如 Node.js EventEmitter 中的 on 和 emit 方法;Vue 中的 $on 和 $emit 方法。他们都使用了发布-订阅模式,让开发变得更加高效方便。 一...

    13651657101 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<