JavaScript设计模式_05_发布订阅模式

发布-订阅模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都将得到通知。发布-订阅模式是使用比较广泛的一种模式,尤其是在异步编程中。

/*
 * pre:发布-订阅模式
 * 一种一对多的关系
 */
// ------ 示例1 ----------
/**
 * 示例:售楼处售楼,购买者询问价格,售楼MM每天要接N多个电话,内容大致都相同,这很是麻烦。
 * 于是售楼MM想到把他们的电话号码记在花名册上,每次一有楼盘的信息,就挨个给他们发消息。
 * 我们将这个过程,抽象为代码如下:
 */
var salesOffice = {};
salesOffice.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);
    }
};
salesOffice.listen(function(price, squareMeter) { // 订阅者1
    console.log("price:" + price);
    console.log("squareMeter:" + squareMeter);
});
salesOffice.listen(function(price, squareMeter) { // 订阅者2
    console.log("定价:" + price);
    console.log("平方数:" + squareMeter);
});
salesOffice.trigger(2000000, 70); // 发布
// ----- 示例2 --------
/* 示例1的不足:订阅者收到了发布者发布的每一条消息,但每个订阅者关注的可能不一样,
 * 比如小明关注80平米左右的房子,小王关注100平米以上的房子。
 * 接下来我们修改程序,示例代码如下:
 */
var salesOffice = {};
salesOffice.clientList = {}; // 使用对象字面量,进行缓存
salesOffice.listen = function(key, fn) {
    if(!this.clientList[key]) {
        this.clientList[key] = [];
    }
    this.clientList[key].push(fn);
};
salesOffice.trigger = function() {
    var key = Array.prototype.shift.apply(arguments);
    if(this.clientList.size == 0 || !this.clientList[key]) {
        console.log("订阅者为空.");
        return;
    }
    for(var fn of this.clientList[key]) {
        fn.apply(this, arguments);
    }
};
salesOffice.listen(8, function(price, squareMeter) {
    console.log("类型:" + 8 + ",price:" + price + ",squareMeter:" + squareMeter);
});
salesOffice.listen(10, function(price, squareMeter) {
    console.log("类型:" + 10 + ",price:" + price + ",squareMeter:" + squareMeter);
});

salesOffice.trigger(8, 2000000, 88);
//----------- 示例3 ------------
/* 试想:如果其他售楼处,也想有发布-订阅功能,是否要将代码重写一次呢?完全没必要。
 * 接下来,我们将上面的代码进行抽象,将客户缓存,以及监听和发布事件提取出来。
 * 给需要的对象进行浅拷贝。
 */
var event = {
    clientList: {},
    listen: function(key, fn) {
        if(!this.clientList.hasOwnProperty(key)) {
            this.clientList[key] = [];
        }
        this.clientList[key].push(fn);
    },
    trigger: function() {
        var key = Array.prototype.shift.call(arguments);
        if(this.clientList.size == 0 || !this.clientList[key]) {
            console.log("订阅者为空.");
            return;
        }
        for(var fn of this.clientList[key]) {
            fn.apply(this, arguments);
        }
    }
};
var installEvent = function(obj) {
    for(var k in event) {
        obj[k] = event[k];
    }
};
var salesOffice = {};
installEvent(salesOffice);
salesOffice.listen(9, function(price, squareMeter) {
    console.log("key:9" + ",price:" + price + ",squareMeter:" + squareMeter);
});
salesOffice.listen(10, function(price, squareMeter) {
    console.log("key:10" + ",price:" + price + ",squareMeter:" + squareMeter);
});
salesOffice.trigger(9, 3000000, 91);
//----------- 示例4 -------------
/* 增加 - 删除订阅功能
 * 如果只传订阅类型,则删除这一组订阅者。
 * 如果传了订阅类型,以及订阅者,则删除该订阅者
 */
var event = {
    clientList: {},
    listen: function(key, fn) {
        if(!this.clientList.hasOwnProperty(key)) {
            this.clientList[key] = [];
        }
        this.clientList[key].push(fn);
    },
    trigger: function() {
        var key = Array.prototype.shift.call(arguments);
        if(this.clientList.size == 0 || !this.clientList[key]) {
            console.log("订阅者为空.");
            return;
        }
        for(var fn of this.clientList[key]) {
            fn.apply(this, arguments);
        }
    }
};
event.remove = function(key, fn) {
    var fns = this.clientList[key];
    if(!fns) {
        return false;
    }
    if(!fn) {
        fns.length = 0;
    } else {
        for(var k in fns) {
            if(fns[k] === fn) {
                fns.splice(k, 1);
                return;
            }
        }
    }
};
var installEvent = function(obj) {
    for(var k in event) {
        obj[k] = event[k];
    }
};
var salesOffice = {};
installEvent(salesOffice);
salesOffice.listen(9, fn1 = function(price, squareMeter) {
    console.log("fn1 - key:9" + ",price:" + price + ",squareMeter:" + squareMeter);
});
salesOffice.listen(9, fn2 = function(price, squareMeter) {
    console.log("fn2 - key:9" + ",price:" + price + ",squareMeter:" + squareMeter);
});
salesOffice.remove(9, fn2);
salesOffice.trigger(9, 2000000, 90);
// -------------- 示例5 --------------
/* [全局发布-订阅]
 * 上面的示例中,如果有两个售楼处,我们就要创建两个对象。现实中,我们会有中介的存在,
 * 我们不要关心是哪个售楼处的楼盘,只要我们跟中介说,有xx平米的房子,就给我发消息。
 * 于是,我们将上面的程序再一次抽象。
 */
var Event = (function() {
    var clientList = {};
    var listen = function(key, fn) {
        if(!clientList.hasOwnProperty(key)) {
            clientList[key] = [];
        }
        clientList[key].push(fn);
    };
    var remove = function(key, fn) {
        var fns = clientList[key];
        if(!fns) {
            return false;
        }
        if(!fn) {
            fns && (fn.length = 0);
        } else {
            for(var a in fns) {
                if(fns[a] === fn) {
                    fns.splice(a, 1);
                    return;
                }
            }
        }
    };
    var trigger = function() {
        var key = Array.prototype.shift.call(arguments);
        var fns = clientList[key];
        if(!fns) {
            return false;
        }
        for(var fn of fns) {
            fn.apply(this, arguments);
        }
    };
    return {
        listen: listen,
        remove: remove,
        trigger: trigger
    }
})();
Event.listen(8, function(price, squareMeter) {
    console.log("key:8," + "price:" + price + ",squareMeter:" + squareMeter);
});
Event.trigger(8, 3000000, 83);
//------------ 示例6 -------------
/*
 * [先发布-后订阅]
 * 在现实中,我们往往需要先发布,后订阅,订阅后消息发送一次
 * 接下来修改程序如下:
 * 
 */
var Event = (function() {
    var clientList = {},
        cache = {},
        listen, remove, trigger;
    listen = function(key, fn) {
        if(!clientList.hasOwnProperty(key)) {
            clientList[key] = [];
        }
        clientList[key].push(fn);
        // 判断是否有未消费的消息
        if(!cache.size != 0) {
            for(var a in cache) {
                var arr = a.split(",");
                if(arr[0] != key) {
                    continue;
                }
                arr.splice(0, 1);// 移除key,只保留价格和平方数
                var fns = cache[a];
                if(fns.length == 0) {// 没有消费者
                    fn.apply(this, arr);
                    fns.push(fn);
                } else {// 消息消费者没有当前的订阅者
                    var flag = false;
                    for(var t of fns) {
                        if(t === fn) {
                            flag = true;
                        }
                    }
                    if(!flag) {
                        fn.apply(this, arr);
                        fns.push(fn);
                    }
                }
            }
        }
    };
    remove = function(key, fn) {
        var fns = clientList[key];
        if(!fns) {
            return false;
        }
        if(!fn) {
            fns && (fn.length = 0);
        } else {
            for(var a in fns) {
                if(fns[a] === fn) {
                    fns.splice(a, 1);
                    return;
                }
            }
        }
    };
    trigger = function() {
        var p = Array.prototype.join.call(arguments, ",");
        var key = Array.prototype.shift.call(arguments);
        var fns = clientList[key];
        cache[p] = [];
        if(!fns) { // 没有订阅者
            return false;
        }
        for(var fn of fns) {
            fn.apply(this, arguments);
            cache[p].push(fn); // 缓存消费的订阅者
        }
    };
    return {
        listen: listen,
        remove: remove,
        trigger: trigger
    }
})();
Event.trigger(8, 3000000, 83);
Event.listen(8, fn1 = function(price, squareMeter) {
    console.log("key:8," + "price:" + price + ",squareMeter:" + squareMeter);
});

// ----------- 示例7 ------------
/* [应用]
 * 在web系统中,当用户登录成功后,我们需要做很多事情,比如在顶部加载头像,刷新地址等。
 * 使用发布-订阅模式,可以帮我们更好的去实现这一功能。
 * 示例如下:
 */
var login = {};
installEvent(login);
var header = (function() {
    login.listen("loginSuc", function(data) {
        header.setAvatar(data.avatar);
    });
    return {
        setAvatar: function(avatar) {
            console.log("设置头像:" + avatar);
        }
    }
})();
var address = (function() {
    login.listen("loginSuc", function() {
        address.refresh();
    });
    return {
        refresh: function() {
            console.log("刷新地址.");
        }
    }
})();
login.trigger("loginSuc", {
    avatar: "xxx"
});
//---------- 示例7 ------------
/* [应用]
 * 模块间通信
 * 示例:页面上有一个按钮,和一个div,我们每点击一次按钮,
 * div里就显示我们点击的次数,我们使用发布-订阅模式实现
 *     <button id="btn">点我</button>
 *    <div id="show"></div>
 */
var a = (function() {
    var div = document.getElementById("show");
    Event.listen("add", function(count) {
        div.innerHTML = count;
    });
})();

var b = (function() {
    var count = 0;
    var btn = document.getElementById("btn");
    btn.onclick = function() {
        Event.trigger("add", ++count);
    }
})();
//=============== 总结 =================
/**
 * 通过以上的示例,我们可以看到发布-订阅的模式,优点十分明显。
 *   优点:1、时间上的解耦;2、对象之间的解耦。非常适用于异步编程,以及对象之间松耦合的实现。
 * 但发布-订阅模式也有很多缺点。
 *   缺点:1、创建订阅者消耗一定的内存和时间,当你订阅一个消息后,可能这个消息至始至终都没有发生,
 * 但订阅者一直存在内存中。
 *        2、发布-订阅模式弱化了对象之间的联系,如果过度使用的话,对象之间的必要联系就会被深埋在背后。
 * 特别是嵌套使用的时候,理解起来就比较费时。
 */
posted @ 2017-06-13 10:18  Stinchan  阅读(191)  评论(0编辑  收藏  举报