zoukankan      html  css  js  c++  java
  • 观察者(发布——订阅)模式

    观察者模式

      观察者模式广泛应用于客户端JavaScript编程中。所有的浏览器事件(鼠标悬停,按键等事件)是该模式的例子。它的另一个名字也称自定义事件,与那些由浏览器触发的相比,自定义事件表示是由你编程实现的事件。此外,该模式的另一个别名是订阅——发布模式
      设计这种模式背后的主要动机是促进形成松散耦合。在这种模式中,并不是一个对象调用另一个对象的方法,而是一个对象订阅另一个对象的特定活动并在状态改变后获得通知。订阅者也称之为观察者,而被观察者的对象称为发布者或主题。当发生了一个重要的事件时,发布者将会通知(调用)所有订阅者并且可能经常以事件对象形式传递消息。

    1. 现实中的观察者模式

      以售楼处为例,小明想要买房,于是招待人员记下小明的手机。小兵,小龙也买房,招待人员获得他们的手机通通给记在花名册上,过几天,有了他们中意的房子,工作人员便会翻开花名册,打电话伺候。
      在这个例子中,小明,小龙,小兵是订阅者,他们订阅房子的信息。售楼处是发布者,一有消息便会依次打电话给购房者。

    2. DOM事件

      实际上,只要我们曾经在DOM节点上面绑定过事件函数,那我们就曾经使用过观察者模式,来看看下面这两句简单的代码发生了什么事情:

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

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

    3. 自定义事件

      除了DOM事件,我们还会经常实现一些自定义的事件,这种依靠自定义事件完成的观察者模式可以用于任何JS代码中。
      现在看看如何一步步实现观察者模式:

    1. 首先要指定好谁充当发布者(比如售楼处)
    2. 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(比如花名册)
    3. 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(打开花名册,依次打电话)

      另外,我们还可以往回调函数里填入一些参数,订阅者可以接受这些参数。这是很有必要的,比如售楼处可以在发给订阅者的短信里加上房子的单价,面积等信息,订阅者接受到这些信息之后可以进行各自的处理:

        var salesOffices = {};  //定义售楼处
        salesOffices.clientList = [];   //缓存列表,存放订阅者的回调函数
        salesOffice.listen = function (fn) {    //增加订阅者
            this.clientList.push(fn);   //订阅的消息添加进缓存列表
        };
        salesOffice.trigger = function () { //发布消息
            for (var i = 0, fn; fn = this.clientList[i++];) {
                fn.apply(this, arguments); //arguments是发布消息时带上的参数
            }
        };
    

      下面我们来进行一些简单的测试:

        var salesOffices = {};  //定义售楼处
        salesOffices.clientList = [];   //缓存列表,存放订阅者的回调函数
        salesOffices.listen = function (fn) {    //增加订阅者
            this.clientList.push(fn);   //订阅的消息添加进缓存列表
        };
        salesOffices.trigger = function () { //发布消息
            for (var i = 0, fn; fn = this.clientList[i++];) {
                fn.apply(this, arguments); //arguments是发布消息时带上的参数
            }
        };
        salesOffices.listen(function (price, squareMeter) { //小明订阅消息
            console.log('a价格= ' + price);
            console.log('squareMeter= ' + squareMeter);
        });
        salesOffices.listen(function (price, squareMeter) { //小龙订阅消息
            console.log('b价格= ' + price);
            console.log('squareMeter= ' + squareMeter);
        });
        salesOffices.trigger(2000000, 88);
        salesOffices.trigger(3000000, 110);
        /*输出:
            a价格= 2000000 
            squareMeter= 88 
            b价格= 2000000 
            squareMeter= 88 
            a价格= 3000000 
            squareMeter= 110 
            b价格= 3000000 
            squareMeter= 110 
        */
    

      至此,我们已经实现了一个最简单的观察者模式,但这里还存在一些问题。我们看到订阅者接收到了发布者发布的每个消息,虽然小明只想买88平米的房子,但是发布者把110平米的信息也推送给了小明,这对小明来说很是麻烦。所以我们有必要增加一个标示key,让订阅者只订阅自己感兴趣的消息。改写后的代码如下:

        var salesOffices = {};  //定义售楼处
        salesOffices.clientList = [];   //缓存列表,存放订阅者的回调函数
        salesOffices.listen = function (key, fn) {    //增加订阅者
            if (this.clientList[key] === undefined) {   //如果还没有订阅过此类消息,给该类消息创建一个缓存列表
                this.clientList[key] = [];
            }
            this.clientList[key].push(fn);   //订阅的消息添加进缓存列表
        };
        salesOffices.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); //arguments是发布消息时带上的参数
            }
        };
        salesOffices.listen('squareMeter88', function (price) { //小明订阅88平米房子的消息
            console.log('a价格= ' + price);
        });
        salesOffices.listen('squareMeter110', function (price) { //小龙订阅110平米房子的消息
            console.log('b价格= ' + price);
        });
        salesOffices.trigger('squareMeter88', 2000000);
        salesOffices.trigger('squareMeter110', 3000000);
        /*输出:
            a价格= 2000000
            b价格= 3000000
        */
    

      现在,订阅者可以只订阅自己感兴趣的事了。

    4. 观察者模式的通用实现

      现在我们已经看到了如何让售楼处拥有接受订阅和发布事件的功能。假设现在小明又去另一个售楼处买房子,那么这段代码是否必须在另一个售楼处对象上重写一次呢,有没有办法可以让所有对象都拥有发布——订阅功能呢?
      答案是有的,JavaScript作为一门解释执行的语言,给对象动态添加职责是理所当然的事情。
      所以我们把发布——订阅的功能提取出来,放在一个单独的对象内:

        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);
                var fns = this.clientList[key];
                if (!fns && fns.length === 0) {
                    return false;
                }
                for (var i = 0, fn; fn = fns[i++];) {
                    fn.apply(this, arguments);
                }
            }
        };
    

      再定义一个installEvent函数,这个函数可以给所有的对象都动态安装发布——订阅功能:

        var installEvent = function (obj) {
            for (var i in event) {
                obj[i] = event[i];
            }
        };
    

      再来测试,我们给售楼处对象salesOffice是动态增加发布——订阅功能:

        var salesOffices = {};
        installEvent(salesOffices);
        salesOffices.listen('squareMeter88', function (price) {
            console.log('a价格= ' + price);
        });
        salesOffices.listen('squareMeter110', function (price) {
            console.log('b价格= ' + price);
        });
        salesOffices.trigger('squareMeter88', 2000000);     //输出:2000000
        salesOffices.trigger('squareMeter110', 3000000);    //输出:3000000 
    

    5. 取消订阅事件

      有时候,我们也许需要取消订阅事件的功能。比如小明突然不想买房子了,为了避免继续接到售楼处的电话,小明需要取消之前订阅的事件。现在我们给event对象增加remove方法:

        event.remove = function (key, fn) {
            var fns = this.clientList[key];
            if (!fns) { //如果key对应的消息没有被人订阅,则直接返回
                return false;
            }
            if (!fn) {  //如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
                fns.length = 0;
            }else{
                for (var i = 0, _fn; _fn = fns[i]; i++) {
                    if (_fn === fn) {
                        fns.splice(i, 1);   //删除订阅者的回调函数
                    }
                }
                
            }
        };
        var installEvent = function (obj) {
            for (var i in event) {
                obj[i] = event[i];
            }
        };
        var salesOffices = {};
        installEvent(salesOffices);
        salesOffices.listen('squareMeter88', fn1 = function (price) {
            console.log('a价格= ' + price);
        });
        salesOffices.listen('squareMeter88', fn2 = function (price) {
            console.log('b价格= ' + price);
        });
        salesOffices.remove('squareMeter88', fn1);
        salesOffices.trigger('squareMeter88', 2000000);     //输出:2000000
    

    6. 全局的发布——订阅对象

      在现实中,买房子未必要亲自去售楼处,我们只要把订阅的请求交给中介公司,而各大房产公司也只需要通过中介公司来发布房子信息。这样一来,我们不用关心消息是来自哪个房产公司,我们在一的是能否顺利接受消息。当然,为了保证订阅者和发布者能顺利通信,订阅者和发布者都必须知道这个中介公司。
      同样在程序中,发布——订阅模式可以用一个全局的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);
                var 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 = this.clientList[key];
                if (!fns) {
                    return false;
                }
                if (!fn) {
                    fns.length = 0;
                }else{
                    for (var i = 0, _fn; _fn = fns[i]; i++) {
                        if (_fn === fn) {
                            fns.splice(i, 1);
                        }
                    }
                
                }
            };
            return {
                listen: listen,
                trigger: trigger,
                remove: remove
            };
        })();
        Event.listen('squareMeter88', function(price) {
            console.log('a价格= ' + price);
        });
        Event.trigger('squareMeter88', 2000000);     //输出:2000000
    

    7. 模块间的通信

      上一节中实现的发布——订阅模式的实现,是基于一个全局的Event对象,我们利用它可以在两个封装良好的模块中进行通信,这两个模块可以完全不知道对方的存在。就如同有了中介公司之后,我们不再需要知道房子的消息来自哪个售楼处。
      比如现在有两个模块,a模块里面有一个按钮,每次点击按钮之后,b模块里的div中会显示按钮的总点击次数,我们用全局发布——订阅模式完成下面的代码,使得a模块和b模块可以在保持封装性的前提下进行通信。

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    </head>
    <body>
        <button id="count">dian我</button>
        <div id="div"></div>
    <script>
        var a = (function () {
            var count = 0;
            var button = document.getElementById('count');
            button.onclick = function() {
                Event.trigger('add', count++);
            };
        })();
        var b = (function () {
            var div = document.getElementById('div');
            Event.listen('add', function (count) {
                div.innerHTML = count;
            });
        })();
    </script>
    </body>
    </html>
    

      但在这里我们要留意另一个问题,模块之间用了太多的全局发布——订阅模式来通信,那么模块与模块之间的联系就被隐藏到了背后。我们最终会搞不清楚消息来自哪个模块,或者消息会流向哪些模块,这又会给我们的维护带来一些麻烦,也许某个模块的作用就是暴露一些借口给其他模块调用。


      参考书目:《JavaScript模式》,《JavaScript设计模式与开发实践》

  • 相关阅读:
    vscode 编写调试autojs
    auto打印调试
    AutoJS 初级操作代码
    转 【海豚教程】用Visual Studio开发安卓应用
    转 android sdk创建AVD时如何更改AVD的存储路径
    安装 Mono for Android for Visual Studio 2010
    转 C# ToolStrip浮动及上/下/左/右 停靠
    关于t328w root后哪些能删除哪些不能删除
    Windows 7 添加 loopback adapter
    如何在vs中创建安装程序
  • 原文地址:https://www.cnblogs.com/fxycm/p/4868458.html
Copyright © 2011-2022 走看看