zoukankan      html  css  js  c++  java
  • 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函数:接触监听函数,为订阅者使用,通常包含一个或两个参数:事件名称或注册监听时的回调函数。若为一个参数事件名称,则会到缓存函数中查找到对应的注册的回调函数的数组,并将其清空。若有第二个参数回调函数,会在缓存对象中找到事件名称对应的回调函数数组,查找是否存在参数中的回调函数,有的话,则只删除这一个回调函数

  • 相关阅读:
    How to function call using 'this' inside forEach loop
    jquery.validate.unobtrusive not working with dynamic injected elements
    Difference between jQuery.extend and jQuery.fn.extend?
    Methods, Computed, and Watchers in Vue.js
    Caution using watchers for objects in Vue
    How to Watch Deep Data Structures in Vue (Arrays and Objects)
    Page: DOMContentLoaded, load, beforeunload, unload
    linux bridge
    linux bridge
    EVE-NG网卡桥接
  • 原文地址:https://www.cnblogs.com/jyybeam/p/13363602.html
Copyright © 2011-2022 走看看