1. 程式人生 > >[轉] 淺析JavaScript設計模式——發布-訂閱/觀察者模式

[轉] 淺析JavaScript設計模式——發布-訂閱/觀察者模式

div 函數 ons class esp 了解 寵物 是我 ring

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

技術分享圖片

觀察者模式的理解

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

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

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

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

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

  • 可以廣泛應用於異步編程,它可以代替我們傳統的回調函數
    我們不需要關註對象在異步執行階段的內部狀態,我們只關心事件完成的時間點
  • 取代對象之間硬編碼通知機制,一個對象不必顯式調用另一個對象的接口,而是松耦合的聯系在一起
    雖然不知道彼此的細節,但不影響相互通信。更重要的是,其中一個對象改變不會影響另一個對象

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

自定義事件

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


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

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

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

//偽代碼
Event = {
    name1: [回調函數1,回調函數2,...],
    name2: [回調函數1,回調函數2,...],
    name3: [回調函數1,回調函數2,...],
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

代碼如下

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(...);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

完整的觀察者對象

我們上面寫的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);

總結

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

  • 時間上解耦
  • 對象間解耦

它應用廣泛,但是也有缺點
創建這個函數同樣需要內存,過度使用會導致難以跟蹤維護

[轉] 淺析JavaScript設計模式——發布-訂閱/觀察者模式