1. 程式人生 > >淺析JavaScript設計模式——釋出-訂閱/觀察者模式

淺析JavaScript設計模式——釋出-訂閱/觀察者模式

觀察者模式
定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知

前一段時間一直在寫CSS3的文章
一直都沒寫設計模式
今天來寫寫大名鼎鼎觀察者模式
先畫張圖

觀察者模式的理解

我覺得還是釋出-訂閱模式的叫法更容易我們理解
(不過也有的書上認為它們是兩種模式……)
這就類似我們在微信平臺訂閱了公眾號
當它有新的文章發表後,就會推送給我們所有訂閱的人

我們可以看到例子中這種模式的優點

  • 我們作為訂閱者不必每次都去檢視這個公眾號有沒有新文章釋出,
    公眾號作為釋出者會在合適時間通知我們
  • 我們與公眾號之間不再強耦合在一起。公眾號不關心誰訂閱了它,
    不管你是男是女還是寵物狗,它只需要定時向所有訂閱者釋出訊息即可

很簡單的道理,過年的時候,群發祝福簡訊一定要比挨個發簡訊方便的多

通過上面的例子映射出我們觀察者模式的優點

  • 可以廣泛應用於非同步程式設計,它可以代替我們傳統的回撥函式
    我們不需要關注物件在非同步執行階段的內部狀態,我們只關心事件完成的時間點
  • 取代物件之間硬編碼通知機制,一個物件不必顯式呼叫另一個物件的介面,而是鬆耦合的聯絡在一起
    雖然不知道彼此的細節,但不影響相互通訊。更重要的是,其中一個物件改變不會影響另一個物件

可能看完這些一臉懵逼,不過沒關係
下面我們會深入體會它的優點

自定義事件

其實觀察者模式我們都曾使用過,就是我們熟悉的事件
但是內建的事件很多時候不能滿足我們的要求
所以我們需要自定義事件

現在我們想實現這樣的功能
定義一個事件物件,它有以下功能

  • 監聽事件(訂閱公眾號)
  • 觸發事件(公眾號釋出)
  • 移除事件(取訂公眾號)

當然我們不可能只訂閱一個公眾號,可能會有很多
所以我們要針對不同的事件設定不同的”鍵”
這樣我們儲存事件的結構應該是這樣的

//虛擬碼
Event = {
    name1: [回撥函式1,回撥函式2,...],
    name2: [回撥函式1,回撥函式2,...],
    name3: [回撥函式1,回撥函式2,...],
}

程式碼如下

var Event = (function(){
    var list = {},
        listen,
        trigger,
        remove;
    listen = function
(key,fn){
//監聽事件函式 if(!list[key]){ list[key] = []; //如果事件列表中還沒有key值名稱空間,建立 } list[key].push(fn); //將回調函式推入物件的“鍵”對應的“值”回撥陣列 }; trigger = function(){ //觸發事件函式 var key = Array.prototype.shift.call(arguments); //第一個引數指定“鍵” msg = list[key]; if(!msg || msg.length === 0){ return false; //如果回撥陣列不存在或為空則返回false } for(var i = 0; i < msg.length; i++){ msg[i].apply(this, arguments); //迴圈回撥陣列執行回撥函式 } }; remove = function(key, fn){ //移除事件函式 var msg = list[key]; if(!msg){ return false; //事件不存在直接返回false } if(!fn){ delete list[key]; //如果沒有後續引數,則刪除整個回撥陣列 }else{ for(var i = 0; i < msg.length; i++){ if(fn === msg[i]){ msg.splice(i, 1); //刪除特定回撥陣列中的回撥函式 } } } }; return { listen: listen, trigger: trigger, remove: remove } })(); var fn = function(data){ console.log(data + '的推送訊息:xxxxxx......'); } Event.listen('某公眾號', fn); Event.trigger('某公眾號', '2016.11.26'); Event.remove('某公眾號', fn);

通過這種全域性的Event物件,我們可以利用它在兩個模組間實現通訊
並且兩個模組互不干擾

//虛擬碼
module1 = function(){
    Event.listen(...);
}
module2 = function(){
    Event.trigger(...);
}

完整的觀察者物件

我們上面寫的Event物件還是存在一些問題

  • 只能先“訂閱”再“釋出”
  • 全域性物件會產生命名衝突

對於第一點,在我們訂閱了一個公眾號之前,它是同樣會定時推送訊息的
我們訂閱後,同樣可以檢視歷史推送
所以我們應該是實現先發布再訂閱仍然可以檢視歷史訊息

對於第二點,如果我們每個人都使用上面我們定義的函式
所有事件都放在一個list當中
時間長了一定會產生命名衝突
最好的辦法就是為物件增加建立名稱空間的功能
現在我們來豐滿我們的事件物件
(這裡我就盜用大神寫的程式碼了,不同的人不同庫實現的方式都不同)
這個完整版稍微瞭解一下就好了

var Event = (function(){
    var global = this,
        Event,
        _default = 'default';
    Event = function(){
        var _listen,
            _trigger,
            _remove,
            _slice = Array.prototype.slice,
            _shift = Array.prototype.shift,
            _unshift = Array.prototype.unshift,
            namespaceCache = {},
            _create,
            find,
            each = function(ary,fn){
                var ret;
                for(var i = 0, l = ary.length; i < l; i++){
                    var n = ary[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; i >= 0; i--){
                            if(cache[key][i] === fn){
                                cache[key].splice(i,1);
                            }
                        }
                    }else{
                        cache[key] = [];
                    }
                }
            };
            _trigger = function(){
                var cache = _shift.call(arguments),
                    key = _shift.call(arguments),
                    args = arguments,
                    _self = this,
                    ret,
                    stack = cache[key];
                if(!stack || !stack.length){
                    return;
                }
                return each(stack,function(){
                    this.apply(_self,args);
                });
            };
            _create = function(namespace){
                var namespace = namespace || _default;
                var cache = {},
                    offlineStack = [],  //離線事件
                    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,
                                _self = this;
                            _unshift.call(arguments,cache);
                            args = arguments;
                            fn = function(){
                                return _trigger.apply(_self,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;
})();
/********* 先發布後訂閱 *********/
Event.trigger('click',1);
Event.listen('click',function(a){
    console.log(a);   //1
});
/********* 使用名稱空間 *********/
Event.create('namespace1').listen('click',function(a){
    console.log(a);   //1
})
Event.create('namespace1').trigger('click',1);
Event.create('namespace3').listen('click',function(a){
    console.log(a);   //2
})
Event.create('namespace3').trigger('click',2);

總結

觀察者模式有兩個明顯的優點

  • 時間上解耦
  • 物件間解耦

它應用廣泛,但是也有缺點
建立這個函式同樣需要記憶體,過度使用會導致難以跟蹤維護