Javascript(七)釋出-訂閱模式

YiHzo發表於2018-11-19

釋出-訂閱模式又叫觀察者模式,它定義與物件間的一種一對多的關係,當一個物件的狀態發生改變時,所有依賴與它的物件都將得到通知。

釋出訂閱的作用

  1. 釋出-訂閱模式可以廣泛應用與非同步程式設計中,這是一種替代傳統回撥函式的方案。

    • 比如訂閱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中的離線訊息,離線訊息先被儲存在伺服器中,接收人下次登陸之後,可以從新接收到這條訊息。

再假如一個商城系統中,獲取到使用者資訊後才能渲染導航模組,無疑這是一個非同步的過程,不能保證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都少不了釋出-訂閱的參與

缺點

建立訂閱者本身要消耗一定時間和記憶體,無論訊息是否發生,一直存在記憶體中。雖然釋出-訂閱模式雖然可以弱化物件之間的聯絡,但是過度使用,物件之間的必然聯絡會被深埋,特別是多個訂閱者和釋出者巢狀會造成難以追蹤的bug。

相關文章