• javascript中的设计模式之发布-订阅模式


    一、定义

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

    二、举例

      js中对dom元素绑定事件监听,就是简单的发布-订阅。另外在很多的框架和插件中都会存在使用这种方式来,比如vue的watch

    三、结构

      发布-订阅模式可以分为两种形式,一种是发布者和订阅者直接进行通信,其结构如下:

     

      另一种是通过中介进行通信,发布者和订阅者互不相知,其结构如下:

    四、实现

    1.发布者和订阅者直接进行通信

      这种模式的核心在于,要在发布者中维护保存一个订阅者的回调函数的数组。

      典型的例子就是绑定dom元素,但是js未将对应的发布者暴露,这是浏览器实现的,因此我们使用的都是直接进行订阅。我们可以自定义一个事件,代码如下:

    // 发布者
    var publisher = {
        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),    // 获取发布的事件名称
                fns = this.clientList[key]; // 获取该事件下所有的回调函数列表
            if(!fns || fns.length === 0){
                return false;
            }
            for(var i = 0, l = fns.length; i < l; i++){
                fns[i].apply(this, arguments);
            }
        },
        run: function(){
            // 发布者根据实际情况在合适时机发布事件
            this.trigger("start_load", "开始加载");
            console.log("start load");
            this.trigger("loading", "正在加载");
            console.log("loading...");
            this.trigger("finish_load", "加载完成");
            console.log("finish load");
        }
    };
    
    // 订阅者
    var subscriber = {
        init: function(){
            // 订阅
            publisher.listen("finish_load", function(rst){
                console.log("我是订阅者,我订阅了发布者的加载完成事件,现在我收到了发布者的信息:" + rst);
            });
        }
    };
    
    subscriber.init();
    
    publisher.run();

      这种的模式很简单,但是他的缺点在于如果有多个发布者,那么就需要让每个发布者维护listen、trigger函数和一个事件回调函数缓存列表,比如我们可以会对js文件的加载过程进行订阅,也可能会对dom的构建过程进行订阅等等,显然每个发布者分别创建一个对象是耗费内存也是不优雅的。另外这种模式存在着发布者和订阅这的耦合性,往往在开发过程中,我们可能根本没有必要让发布者和订阅者进行通信,各自做好自己的事情就好了。因此这种方式很少会用到。

    2.通过中介进行通信

      针对上面的方式的缺点,就有了这种方式,这样的模式是一种全局的发布-订阅模式。其核心是创建一个中介,也就是一个全局的Event对象,让他来帮助发布者和订阅者沟通。代码如下:

    // 事件对象,作为中介
    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),    // 获取发布的事件名称
                fns = this.clientList[key]; // 获取该事件下所有的回调函数列表
            if(!fns || fns.length === 0){
                return false;
            }
            for(var i = 0, l = fns.length; i < l; i++){
                fns[i].apply(this, arguments);
            }
        }
    };
    // 发布者
    var publisher = {
        run: function(){
            Event.trigger("start_load", "开始加载");
            console.log("start load");
            Event.trigger("loading", "正在加载");
            console.log("loading...");
            Event.trigger("finish_load", "加载完成");
            console.log("finish load");
        }
    };
    
    // 订阅者
    var subscriber = {
        init: function(){
            // 订阅
            Event.listen("finish_load", function(rst){
                console.log("我是订阅者,我订阅了发布者的加载完成事件,现在我收到了发布者的信息:" + rst);
            });
        }
    };
    subscriber.init();
    publisher.run();

      我们看绘制二维地图的leaflet框架中,对于发布-订阅模式的实现:

    export var Events = {
        // 添加监听事件,types:{mouseclick: fn, dbclick: fn} 或者:"mouseclick dbclick"
        on: function (types, fn, context) {...},
        // 移除事件,若未设置任何参数,则删除该对象所有的事件。若fn未设置,则删除对象中所有的type事件
        off: function (types, fn, context) {...},
    
        // 内部注册监听事件
        _on: function (type, fn, context) {
            this._events = this._events || {};
            var typeListeners = this._events[type];    // 获取对象中其他注册的该事件的回调函数
            // 若对象中未曾设置过相同事件名称,则保存其回调函数
            if (!typeListeners) {
                typeListeners = [];
                this._events[type] = typeListeners;
            }
    
            var newListener = {fn: fn, ctx: context};
            ...
            typeListeners.push(newListener);
        },
    
        // 移除事件, 若未设置fn则删除所有的type事件。,否则删除对应的事件
        _off: function (type, fn, context) {...},
    
        // 触发对象中的所有type事件,若设置了propagate则触发父对象的type事件
        fire: function (type, data, propagate) {
            if (!this.listens(type, propagate)) { return this; }   // 检查是否注册了type事件
            // 构建回调函数中参数事件对象
            var event = Util.extend({}, data, {
                type: type,
                target: this,
                sourceTarget: data && data.sourceTarget || this
            });
            if (this._events) {
                var listeners = this._events[type];
    
                // _firingCount用于防止触发事件未执行完成同时删除该事件。
                // _firingCound表示正在执行的回调函数的个数,当为0时表示没有正在执行的事件。可以直接删除,否则需要将_events进行复制,防止删除掉需要回调的对象
                if (listeners) {
                    this._firingCount = (this._firingCount + 1) || 1;
                    // 执行对象注册的所有该事件的回调函数
                    for (var i = 0, len = listeners.length; i < len; i++) {
                        var l = listeners[i];
                        l.fn.call(l.ctx || this, event);
                    }
                    this._firingCount--;
                }
            }
            return this;
        },
    
        listens: function (type) {
            var listeners = this._events && this._events[type];
            return !!(listeners && listeners.length);
        },
    };
    export var Evented = Class.extend(Events);

      使用Event基础类用来实现发布-订阅,而这个基础类类似于一个接口,他需要依附于一个实际的对象来构造该对象的事件系统,比如框架中的图层需要有各种鼠标、键盘触摸事件,因此为了模仿类似dom一样的触发方式,图层类就要继承这个Event类:

     

      因此在订阅的时候,就可以直接:

    layer.on("click",function(){});

    五、总结

    发布-订阅模式的关键在于用一个事件对象对其进行实现,其中需要有以下几点:

      1.缓存对象:用于保存订阅者监听的回调函数,键为事件名称,值为该事件名下所有的回调函数,其结构形如:

    var cache = {
        "click": [fn1,fn2,fn3...],
        "dbclick": [fn4,fn5,fn6...]
            ...
    }

      2.listen/on函数:监听函数,为订阅者使用,通常包含两个参数:事件名称和回调函数,内部会将这两个参数保存到缓存对象中

      3.trigger/fire函数:发布函数,为发布者使用,通常包含一个参数:事件名称。内部通过事件名称到缓存对象中查找对应的回调函数数组,并依次执行

      4.remoe/off函数:接触监听函数,为订阅者使用,通常包含一个或两个参数:事件名称或注册监听时的回调函数。若为一个参数事件名称,则会到缓存函数中查找到对应的注册的回调函数的数组,并将其清空。若有第二个参数回调函数,会在缓存对象中找到事件名称对应的回调函数数组,查找是否存在参数中的回调函数,有的话,则只删除这一个回调函数

  • 相关阅读:
    第2章安装和升级MySQL
    1.7.3.4 ENUM和SET约束
    1.7.3.3对无效数据的强制约束
    1.7.3.2外部关键约束
    跨浏览器的事件处理程序-读书笔记
    表单-读书笔记
    【小知识点】一条线的居中问题
    函数表达书-读书笔记
    原型链-读书笔记
    面向对象(三)-读书笔记
  • 原文地址:https://www.cnblogs.com/jyybeam/p/13363602.html
Copyright © 2020-2023  润新知