js之发布 — 订阅模式

有人发布信息有人“订阅”信息并进行处理。

DOM事件

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

售楼处level01

售楼处发布楼盘信息,订阅者订阅(即把你需要的行为存入售楼处)这些信息。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        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 ); // (2) // arguments 是发布消息时带上的参数
            }
        };
//        下面我们来进行一些简单的测试:
        salesOffices.listen( function( price, squareMeter ){ // 小明订阅消息
            console.log( '价格= ' + price );
            console.log( 'squareMeter= ' + squareMeter );
        });

        salesOffices.trigger( 2000000, 88 ); // 输出:200 万,88 平方米
        salesOffices.trigger( 3000000, 110 ); // 输出:300 万,110 平方米
    </script>
</body>
</html>

缺点:没有订阅的也会发来。


售楼处level02

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        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 ), // 取出消息类型
                fns = this.clientList[ key ]; // 取出该消息对应的回调函数集合
            if ( !fns || fns.length === 0 ){ // 如果没有订阅该消息,则返回
                return false;
            }
            for( var i = 0, fn; fn = fns[ i++ ]; ){
                fn.apply( this, arguments ); // (2) // arguments 是发布消息时附送的参数
            }
        };
        salesOffices.listen( 'squareMeter88', function( price ){ // 小明订阅 88 平方米房子的消息
            console.log( '价格= ' + price ); // 输出: 2000000
        });

        salesOffices.listen( 'squareMeter110', function( price ){ // 小红订阅 110 平方米房子的消息
            console.log( '价格= ' + price ); // 输出: 3000000
        });
        salesOffices.trigger( 'squareMeter88', 2000000 ); // 发布 88 平方米房子的价格
        salesOffices.trigger( 'squareMeter110', 3000000 ); // 发布 110 平方米房子的价格
    </script>
</body>
</html>

解决了level01的问题
接受者只要有相同key的的接受者都可以接受到信息。


网站登录level01

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

等到有一天,项目中又新增了一个收货地址管理的模块,这个模块本来是另一个同事所写的,而此时你正在马来西亚度假,但是他却不得不给你打电话:“Hi,登录之后麻烦刷新一下收货地址列表。”于是你又翻开你 3个月前写的登录模块,在最后部分加上这行代码:

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

网站登录level02

用发布 — 订阅模式重写之后,对用户信息感兴趣的业务模块将自行订阅登录成功的消息事件。当登录成功时,登录模块只需要发布登录成功的消息,而业务方接受到消息之后,就会开始进行
各自的业务处理,登录模块并不关心业务方究竟要做什么,也不想去了解它们的内部细节。改善后的代码如下:

<script>
       /* login.succ(function(data){
            header.setAvatar( data.avatar); // 设置 header 模块的头像
            nav.setAvatar( data.avatar ); // 设置导航模块的头像
            message.refresh(); // 刷新消息列表
            cart.refresh(); // 刷新购物车列表
        });*/
        $.ajax( 'http:// xxx.com?login', function(data){ // 登录成功
            login.trigger( 'loginSucc', data); // 发布登录成功的消息
        });
        var header = (function(){ // header 模块
            login.listen( 'loginSucc', function( data){
                header.setAvatar( data.avatar );
            });
            return {
                setAvatar: function( data ){
                    console.log( '设置 header 模块的头像' );
                }
            }
        })();
        var nav = (function(){ // nav 模块
            login.listen( 'loginSucc', function( data ){
                nav.setAvatar( data.avatar );
            });
            return {
                setAvatar: function( avatar ){
                    console.log( '设置 nav 模块的头像' );
                }
            }
        })();

     /*   如果有一天在登录完成之
        后,又增加一个刷新收货地址列表的行为,那么只要在收货地址模块里加上监听消息的方法即可,
        而这可以让开发该模块的同事自己完成,你作为登录模块的开发者,永远不用再关心这些行为了。
        代码如下:*/
        var address = (function(){ // nav 模块
            login.listen( 'loginSucc', function( obj ){
                address.refresh( obj );
            });
            return {
                refresh: function( avatar ){
                    console.log( '刷新收货地址列表' );
                }
            }
        })();

售楼处 level03:全局的发布 - 订阅对象

发布 — 订阅模式可以用一个全局的 Event 对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者, Event 作为一个类似“中介者”的角色,把订阅者和发布者联系起来。见如下代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        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 ),
                    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: listen,
                trigger: trigger,
                remove: remove
            }
        })();
        Event.listen( 'squareMeter88', function( price ){ // 小红订阅消息
            console.log( '价格= ' + price ); // 输出:'价格=2000000'
        });

        Event.listen( 'squareMeter88', function( price ){ // 小红订阅消息
            console.log( '价格= ' + price ); // 输出:'价格=2000000'
        });
        Event.listen( 'squareMeter88', fn=function( price ){ // 小红订阅消息
            console.log( '价格= ' + price ); // 输出:'价格=2000000'
        });
        Event.remove( 'squareMeter88');

        Event.trigger( 'squareMeter88', 2000000 ); // 售楼处发布消息
    </script>
</body>
</html>

网站登录level04

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="PublishSubscribe.js"></script>
</head>
<body>
    <script>
        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 ), // (1);
                    fns = this.clientList[ key ];
                if ( !fns || fns.length === 0 ){ // 如果没有绑定对应的消息
                    return false;
                }
                for( var i = 0, fn; fn = fns[ i++ ]; ){
                    fn.apply( this, arguments ); // (2) // arguments 是 trigger 时带上的参数
                }
            },
            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 );
                        }
                    }
                }
            }
        };
        var installEvent = function( obj ){
            for ( var i in event ){
                obj[ i ] = event[ i ];
            }
        };
        var salesOffices = {};
        installEvent( salesOffices );
        salesOffices.listen( 'squareMeter88', function( price ){ // 小明订阅消息
            console.log( '价格= ' + price );
        });
        salesOffices.listen( 'squareMeter100', function( price ){ // 小红订阅消息
            console.log( '价格= ' + price );
        });
        /*salesOffices.listen( 'squareMeter88', function( price ){ // 小红订阅消息
            console.log( '价格= ' + price ); // 输出:'价格=2000000'
        });
         salesOffices.listen( 'squareMeter88', fn=function( price ){ // 小红订阅消息
            console.log( '价格= ' + price ); // 输出:'价格=2000000'
        });
        Event.remove( 'squareMeter88');*/
        salesOffices.trigger( 'squareMeter88', 2000000 ); // 输出:2000000
        salesOffices.trigger( 'squareMeter100', 3000000 ); // 输出:3000000
    </script>
</body>
</html>

模块间通信

上一节中实现的发布 — 订阅模式的实现,是基于一个全局的 Event 对象,我们利用它可以在两个封装良好的模块中进行通信,这两个模块可以完全不知道。

比如现在有两个模块,a模块里面有一个按钮,每次点击按钮之后,b模块里的 div中会显示按钮的总点击次数,我们用全局发布 — 订阅模式完成下面的代码,使得 a 模块和 b 模块可以在保
持封装性的前提下进行通信。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="PublishSubscribe.js"></script>
</head>
<body>
    <button id="count">点我</button>
    <div id="show"></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( 'show' );
            Event.listen( 'add', function( count ){
                div.innerHTML = count;
            });
        })();
    </script>
</body>
</html>

即发布和订阅在不同的两个模块里。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章