Javascript(七)發佈-訂閱模式

發佈-訂閱模式又叫觀察者模式,它定義與對象間的一種一對多的關係,當一個對象的狀態發生改變時,全部依賴與它的對象都將獲得通知。

發佈訂閱的做用

  1. 發佈-訂閱模式能夠普遍應用與異步編程中,這是一種替代傳統回調函數的方案。ajax

    • 好比訂閱ajax的succ,error等事件,只需訂閱事件發生點,而無需關注異步運行期間狀態
  2. 發佈-訂閱能夠取代對象之間硬編碼的通訊機制,一個對象再也不顯式地調用另一個對象的某個接口。編程

    • 發佈-訂閱模式讓兩個對象鬆散耦合地聯繫在一塊兒,雖然不清楚彼此的細節,但不影響它們之間的通訊。
    • 當有新的訂閱者出現時,發佈者的代碼不須要任何修改;一樣發佈者改變時,也不會影響到以前的訂閱者。

發佈-訂閱模式的通用實現

var event = {
    //緩存列表,存放訂閱者的回調函數
    clientList: [],
    //增長訂閱者, key:事件名,fn:回調函數
    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, fn; fn = fns[i++];) {
            fn.apply(this, arguments)
        }
    },
    //取消訂閱
    remove: function(key, fn) {
        var fns = this.clientList[key]
        // 若是對應的消息沒人訂閱,直接返回
        if(!fns) {    
            return false
        }
        // 若是沒有傳入回調函數,表示取消key對應消息的全部訂閱
        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 salesOffices = {}
// 給對象安裝發佈-訂閱功能
var installEvent = function(obj){
    for(var i in event){
        obj[i] = event[i]
    }
}
installEvent(salesOffices)


salesOffices.listen('squareMeter88', fn1 = function(price){
    console.log('價格1:'+price)
})

salesOffices.listen('squareMeter87', fn2 = function(price){
    console.log('價格2:'+price)
})    

salesOffices.trigger('squareMeter88', 20000) // 價格1: 20000

salesOffices.remove('squareMeter88')

salesOffices.trigger('squareMeter88', fn1)  // 打印結果爲空

其實在這個發佈-訂閱模式中還存在兩個問題:緩存

  1. 給對象單獨添加實際上是一種資源浪費
  2. 發佈者與訂閱者存在必定耦合性,至少要知道salesOffices這個對象

因此接下來實現一個全局的發佈-訂閱對象服務器

var Event = (function(){
    var clientList = {},
        listten,
        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,
        trigger,
        remove
    }
})()
Event.listen('squareMeter88', fn1 = function(price){ // 訂閱消息
    console.log('價格1:'+price)
})
Event.trigger('squareMeter88', 20000) // 價格1: 20000

必須先訂閱在發佈嗎

咱們所瞭解的訂閱發佈都是訂閱者必須先訂閱一個消息,隨後才能接受到發佈者發佈的消息。若是順序反過來,發佈者先發布一條消息,而此時沒有訂閱者,無疑消息會消失。架構

探討下這種需求

在某些狀況下,咱們須要先講這條消息保存下來,等到有對象訂閱它的時候,再從新把消息發佈給訂閱者,若是QQ中的離線消息,離線消息先被保存在服務器中,接收人下次登錄以後,能夠重新接收到這條消息。app

再假如一個商城系統中,獲取到用戶信息後才能渲染導航模塊,無疑這是一個異步的過程,不能保證ajax什麼時候返回。因此咱們須要咱們發佈訂閱對象擁有先發布後訂閱的能力。異步

如何實現呢

爲了知足這種須要,咱們要創建一個存放離線事件的堆棧,當事件發佈的時候,若是此時尚未訂閱者來訂閱這個事件,咱們暫時把發佈事件的動做包裹在一個函數裏,這些包裹函數被存入堆棧中,等到終於有對象來訂閱此事件,咱們將遍歷堆棧而且依次執行這些包裝函數,也就是從新發布里面的事件。固然離線事件的生命週期只有一次,就像QQ未讀消息只會被從新閱讀一次。異步編程

命名衝突

全局的訂閱-發佈對象裏只有一個clientList來存放消息名和回調函數,咱們都經過它來訂閱和發佈各類消息,長此以往,不免會出現事件名衝突的狀況,因此須要給Event對象提供建立命名空間的功能。函數

具體實現
var Event = (function() {

    var Event,
        _default = 'default';

    Event = function() {
        var _listen,
            _trigger,
            _remove,
            _shift = [].shift,
            _unshift = [].unshift,
            namespaceCache = {},
            _create,
            each = function(arr, fn) {
                var ret;
                for (var i = 0, l = arr.length; i < l; i++) {
                    var n = arr[i];
                    ret = fn.call(n, i, n);
                }

                return ret;
            };

        _listen = function(key, fn, cache) {
            if (!cache[key]) {
                cache[key] = [];
            }
            cache[key].push(fn);
        };

        _remove = function(key, cache, fn) {
            if (cache[key]) {
                if (fn) {
                    for (var i = cache[key].length - 1; i >= 0; i--) {
                        if (cache[key][i] === fn) { 
                            // 刪除訂閱回調函數
                            cache[key].splice(i, 1);
                        }
                    }
                }
                else {
                    cache[key] = [];
                }
            }
        };

        _trigger = function() {
            var cache = _shift.call(arguments);
            var key = _shift.call(arguments);
            var args = arguments;
            var _this = this;
            var stack = cache[key];

            if (!stack || !stack.length) {
                return;
            }

            return each(stack, function() {
                return this.apply(_this, args);
            })
        };

        _create = function(namespace) {
            namespace = namespace || _default;
            var cache = {};
            var offlineStack = []; //離線事件
            var ret = {
                listen: function(key, fn, last) {
                    _listen(key, fn, cache);

                    if (offlineStack === null) {
                        return;
                    }

                    if (last === 'last') {
                        offlineStack.length && offlineStack.pop()();
                    }
                    else {
                        each(offlineStack, function() {
                            this();
                        });
                    }

                    offlineStack = null;
                },
                one: function(key, fn, last) {
                    _remove(key, cache);
                    this.listen(key, fn, last);
                },
                remove: function(key, fn) {
                    _remove(key, cache, fn);
                },
                trigger: function() {
                    var fn,
                        args,
                        _this = this;

                    _unshift.call(arguments, cache);
                    args = arguments;

                    fn = function() {
                        return _trigger.apply(_this, args);
                    };

                    if (offlineStack) {
                        return offlineStack.push(fn);
                    }

                    return fn();
                }
            };

            return namespace ?
                (namespaceCache[namespace] ?
                    namespaceCache[namespace] : namespaceCache[namespace] = ret)
                : ret;
        };

        return {
            create: _create,
            one: function(key, fn, last) {
                var event = this.create();
                event.one(key, fn, last);
            },
            remove: function(key, fn) {
                var event = this.create();
                event.remove(key, fn);
            },
            listen: function(key, fn, last) {
                var event = this.create();
                event.listen(key, fn, last);
            },
            trigger: function() {
                var event = this.create();
                event.trigger.apply(this, arguments);
            }
        }
    }();

    return Event;
}());

var fn1 = function(price) {
    console.log(price);
};

// 實例
Event.listen('squareMeter88', fn1);
Event.remove('squareMeter88', fn1);

Event.listen('squareMeter88', function(price) {
    console.log('fn2: ' + price);
});


Event.trigger('squareMeter88', 20000);   // fn2: 20000

總結

優勢

時間與對象之間的解耦,應用很是普遍,既能夠用在異步編程中,也能夠幫助咱們完成更鬆耦合的代碼編寫,從架構上看,不管MVC仍是MVVM都少不了發佈-訂閱的參與this

缺點

建立訂閱者自己要消耗必定時間和內存,不管消息是否發生,一直存在內存中。雖然發佈-訂閱模式雖然能夠弱化對象之間的聯繫,可是過分使用,對象之間的必然聯繫會被深埋,特別是多個訂閱者和發佈者嵌套會形成難以追蹤的bug。

相關文章
相關標籤/搜索