1. 程式人生 > >JavaScript的事件廣播與偵聽

JavaScript的事件廣播與偵聽

先來看html頁面的主要結構:

<div id="wrap" class="wrap">
	<input type="text" name="test" id="test" size="35" />
</div>

<div class="wrap" style="position:absolute;left:20px;top:250px;">
	left:<div id="left" class="inner"></div>
</div>
<div class="wrap" style="position:absolute;left:550px;top:250px;">
	right:<div id="right" class="inner"></div>
</div>
需求:id為test的輸入框(以下簡稱A)值改變時,實時改變id為left和right的內容。

如果這是一個小專案,一個簡單的頁面,那這個實現這個需求是非常簡單的。但如果是一個比較複雜的專案,處理left和right都各有很多業務邏輯,需要把這兩部分放在不同模組裡,那麼這時候就不得不考慮一種新的解決方案。而且隨著時間的增加,可能在某天產品又會增加一個新模組,也要求A的值變化時作出相應反應。這時候,問題就變得越來越複雜了。

事件廣播和偵聽,是實現這種需求最好的方案。

那什麼是事件廣播和偵聽,我個人的理解是:當發生某一事件時,在某個頻道上發出廣播,向註冊到該頻道上的所有監聽者傳送資訊資料,讓各監聽者在自己內部做相應處理。

這樣做可以解耦各模組,很方便地拆卸每個模組。

下面我們用事件廣播和偵聽來實現上面的需求,步驟如下:

1、建立一個頻道

//頻道說明,建立一個頻道時寫清楚
//說明這個頻道是幹什麼的;什麼時候會廣播這個頻道;廣播時傳的資料結構或引數形式,等等
Utils.Listener.createChannel('index.test.change'); //可以使用這種類似名稱空間的字串做頻道名,這裡表示index頁下的test發生改變
2、在不同程式碼模組裡向頻道index.test.change新增監聽者。這裡監聽者的名字是changeLeft,第三個引數是收到廣播訊息時應該做什麼處理,即回撥函式。
    Utils.Listener.add('index.test.change', 'changeLeft', function(data){
        
        $E('left').innerHTML = data; //改變left的內容
        
        //Utils.Listener.remove('index.test.change', 'changeLeft'); //移除頻道監聽
        
        //Utils.Listener.remove('index.test.change'); //刪除整個頻道
        
    });
新增另外一個監聽者changeRight,當然在一個頻道上你可以新增很多很多的監聽者:
    Utils.Listener.add('index.test.change', 'changeRight', function(data){
        
        $E('right').innerHTML = data;
        
    });
3、給A框繫結keyup事件,讓它的值發生變化時發出廣播
    Core.Events.addEvent($E('test'), function(){
        //在頻道 index.test.change 發出廣播
        Utils.Listener.broadcast('index.test.change', $E('test').value); //第二個引數為廣播時發出的資訊,你可以新增多個引數
        
        //在頻道 index.test.change 向監聽者 changeLeft 發出單播
        //Utils.Listener.unicast('index.test.change', 'changeLeft', $E('test').value+'. Wahaha~');
        
    }, 'keyup');
大功告成!改變A框的值時,left和right已能跟隨改變內容。

當然,事件廣播和偵聽的功能遠遠不只這個,你可以用它來實現各相互獨立的業務模組之間的通訊,等等。還有,做web編輯器的時候,也需要用到,因為你可能要在iframe的body上多次監聽keyup之類的事件,如果每次都綁事件,那效率太低了,所以如果使用事件廣播的話,只需要在body上綁一個keyup事件,然後廣播這個事件就行了。

最後把廣播和偵聽的程式碼貼出來:

/**
 * 事件偵聽、廣播、單播
 * @method
 * @example
        //建立頻道“unameChange”
        Utils.Listener.createChannel('unameChange');
        //建立頻道“uname.change”---天然支援“偽”名稱空間!!!
        Utils.Listener.createChannel('uname.change');

        //新增unameChange的監聽者“updateTray”
        Utils.Listener.add('unameChange', 'updateTray', function(){
            //TODO
        });
        //新增unameChange的監聽者“changeFootbar”
        Utils.Listener.add('unameChange', 'changeFootbar', function(){
            //TODO
        });
        //發出廣播
        Utils.Listener.broadcast('unameChange', someData);
        //發出廣播
        Utils.Listener.broadcast('uname.change', someData);
        //發出單播
        Utils.Listener.unicast('uname.change', 'changeFootbar', someData);
 */
var Utils = {};
!function(){
    if(Utils.Listener){
        return;
    }
    var _channels = {},
        slice = Array.prototype.slice;

    Utils.Listener = {
        //channelName 頻道名,天然支援“偽”名稱空間。例如:uname.change
        createChannel: function(channelName){
            if( _channels[channelName] ){
                traceError('Channel "'+channelName+'" has been defined!');
            }else{
                _channels[channelName] = {};
            }
        },
        //channelName 監聽頻道
        //listenerName 監聽者
        //handler 發生廣播時的執行函式
        add: function(channelName, listenerName, handler){
            var channel = _channels[channelName];
            if( !channel ){
                traceError('Channel "'+channelName+'" has NOT been defined!');
                return;
            }
            if( channel[listenerName] ){
                traceError(channelName+':'+listenerName+'" has been defined!');
                return;
            }
            channel[listenerName] = handler;
        },
        broadcast: function(channelName/*, data...*/){
            var channel = _channels[channelName];
            if( channel ){
                for(var p in channel){
                    if( channel[p] ){
                        channel[p].apply(null, slice.call(arguments,1));
                    }
                }
            }
        },
        unicast: function(channelName, listenerName/*, data...*/){
            var channel = _channels[channelName];
            if( channel && channel[listenerName]){
                channel[listenerName].apply(null, slice.call(arguments,2));
            }
        },
        //channelName 頻道名
        //listenerName 可選,如果沒有,將刪除整個頻道
        remove: function(channelName, listenerName){
            var channel = _channels[channelName];
            if( channel ){
                if(listenerName){
                    channel[listenerName] = null;
                    delete channel[listenerName];
                }else{
                    channel = null;
                    delete _channels[channelName];
                }
            }
        }
        
    };

}();

歡迎大家提出改進建議。