1. 程式人生 > 其它 >觀察者模式(JS描述)

觀察者模式(JS描述)

觀察者模式又名"釋出-訂閱"模式,能夠有效的將模組間進行解耦。

觀察者模式是運用在一些有一對多關係的場景中,一個物件有了一些改變,其他依賴於該物件的物件也要做一些動作。

比如,上課,老師提了一個問題,所有會這道問題的學生都解答這個問題。其中老師就是主物件,老師提問就是狀態改變,學生是依賴於老師的物件,學生回答問題就是針對老師提問這個狀態所做的改變。

弄清楚這個原理,我們來看看怎麼用程式碼實現。

我們可以建立一個物件幫我們監控主物件得改變,而且要通知訂閱了主物件改變這個時間的依賴物件做出相應,還以上面老師提問學生答為例,我們創一個Observer的物件,它得能幫學生去訂閱老師提問某個問題,也得能幫老師發出提問得訊息,而且既然有訂閱,就得有解除訂閱,所以得有一個解除訂閱得方法,基於這個思路,我們設計的Observer差不多就是以下這個樣子:

const Observer = (function(){
    var _messages = {};
    return {
        add: ()=>{},   //訂閱訊息
        emit: ()=>{},  //釋出訊息
        remove: ()=>{} //取消訊息的訂閱
    }
})()

用立即執行函式建立一個私有變數,_messages存放被訂閱的訊息,以及訂閱者所要執行的操作。

接下來我們看add方法,add要把被訂閱的訊息,以及訂閱者的操作存放到 _messages 裡,以供在訊息發出時,訂閱者能夠做出相應迴應。所以add方法可以這麼實現:

add: (type, fn)=>{        //type就是訊息,fn就是訂閱者的回撥
    if(_messages[type]){
        _messages[type].push(fn)
    }else{
        _messages[type] = [fn] 
    }
}

方法很簡單,就是看_messages裡有沒有某個訊息,如果沒有就建立陣列,把回撥方里,否者直接放數組裡。因為可能有多個訂閱者訂閱同一個訊息,所以要用陣列。

接下來是emit方法,emit用於發出某個訊息,其實就是去_messages中看某個訊息有沒有訂閱者,有的話就把所有的回撥執行一遍。

emit: (type, args)=>{
    if(_messages[type] && _messages[type] instanceof Array){
        for(let i = 0; i < _messages[type].length; i++){
                    typeof _messages[type][i] == 'function' && _messages[type][i].call(this, args)
        }
    }
}

最後就是remove方法,其實就是看某個訊息找某個訂閱者的回撥,刪掉就行。

remove: (type, fn)=>{
   if(_messages[type] && _messages[type] instanceof Array){
      for(let i = _messages[type].length - 1; i >= 0; i--){
           _messages[type][i] == fn && _messages[type].splice(i, 1);
       }
    }
}

組合起來,一個完整的觀察者物件就是下面這樣:

const Observer = (function(){
    var _messages = {};
    return {
        add: (type, fn)=>{
            if(_messages[type]){
                _messages[type].push(fn)
            }else{
                _messages[type] = [fn] 
            }
        },
        emit: (type, args)=>{
            if(_messages[type] && _messages[type] instanceof Array){
                for(let i = 0; i < _messages[type].length; i++){
                    typeof _messages[type][i] == 'function' && _messages[type][i].call(this, args)
                }
            }
        },
        remove: (type, fn)=>{
            if(_messages[type] && _messages[type] instanceof Array){
                for(let i = _messages[type].length - 1; i >= 0; i--){
                    _messages[type][i] == fn && _messages[type].splice(i, 1);
                }
            }
        }
    }
})()

接下來,讓我們看看它是怎麼工作的,還以學生答問題為例,我們先來一個老師,老師就是釋出提問:

var Teacher = function(){
    this.question = function(questionName){
        console.log('老師提問了: ' + questionName);
        Observer.emit(questionName, {question: questionName})
    }
}

然後是學生,學生有一個回答問題的方法,有一個監聽問題的方法,只監聽自己會的問題,另外還有一個睡覺方法,睡覺其實就是取消訂閱某個訊息。

var Student = function(name){
    this.name = name;
    this.answer = function(args){
        console.log(name +'回答老師的問題: ' + args.question);
    }
}
Student.prototype = {
    listen: function(questionName){
        console.log(this.name +'想要回答問題: ' + questionName);
        Observer.add(questionName, this.answer)
    },
    sleep: function(questionName){
        console.log(this.name +'睡著了');
        Observer.remove(questionName, this.answer)
    }
}

listen和sleep可以放到原型鏈上,但是answer方法放在例項上了,因為如果再放在原型鏈上,觀察者就沒法分清哪個回撥是哪個訂閱者的了。

有了以上兩個物件,我們結合下面的程式碼,看看觀察者怎麼工作:

var s1 = new Student('小明');
var s2 = new Student('小紅');

s1.listen("誰最帥");
s2.listen("誰最帥");

var t = new Teacher();
setTimeout(() => {
    t.question('誰最帥');

    s1.sleep("誰最帥");
    setTimeout(() => {
        t.question('誰最帥')
    }, 2000);

}, 2000);

我們建立兩個學生,分別監聽了'誰最帥'這個問題,然後建立一個老師,老師兩秒後提問'誰最帥',兩個學生應該都回答該問題。然後學生s1睡著了,於是不再監聽'誰最帥',老師2秒後又提問,這次只有學生s2回答該問題。
完整的結果是下面這樣的。

小明想要回答問題: 誰最帥
小紅想要回答問題: 誰最帥
老師提問了: 誰最帥

小明回答老師的問題: 誰最帥
小紅回答老師的問題: 誰最帥
小明睡著了

老師提問了: 誰最帥
小紅回答老師的問題: 誰最帥

會出現上面的結果就是觀察者在起作用,兩個學生監聽提問,Observer往_messages中插入了'誰最帥',而且把兩位學生的回答方法放進了數組裡,等老師提問,Observer就去_messages中依次執行。後來s1取消了訂閱,Observer從_messages刪除了該物件對事件的訂閱,從而第二次提問的時候,s1就不再回答了。