用最簡單的方式聊一下JavaScript中的觀察者模式

觀察者模式,是JavaScript設計模式之一。固然也不只僅限於JavaScript這門語言,網上對該模式的介紹已經是多如牛毛,並且講得各有特點各有心得。即使如此,筆者仍精心準備了這篇博客,指望用最簡單的方式來介紹下該模式。前端

首先來看下維基百科對 觀察者模式 的解釋:git

觀察者模式是軟件設計模式的一種。在此種模式中,一個目標對象管理全部相依於它的觀察者對象,而且在它自己的狀態改變時主動發出通知。這一般透過呼叫各觀察者所提供的方法來實現。此種模式一般被用來實時事件處理系統。

其實筆者更傾向於它的另外一個名字發佈/訂閱模式(Publish/Subscribe),由於更能表達出該模式的核心思路,那就是:發佈訂閱兩個過程。是否是還感受模棱兩可?不用擔憂,下面就用我們身邊發生的事情來作個形象化的解釋:
你們都有訂閱網站郵件的經歷吧?若是你沒有的話,emmmmm....那就繼續往下看吧哈哈!!
假如我今天想訂閱xxx公司的郵件,那麼這裏就涉及到兩個對象:xxx公司
從行爲上來看就是我訂閱了xxx公司郵件,xxx公司會發送郵件給個人郵箱。但某天我不想再收到xxx公司的郵件了,那麼我能夠取消訂閱,這樣xxx公司就不會再發郵件到個人郵箱。設計模式

說到這裏,是否是就有點眉頭了呢?好,咱們繼續往下說,
經過剛剛的形象化解釋,咱們能夠羅列下觀察者模式的一些核心的東西:數組

對象:我 (訂閱者), xxx公司 (發佈者), 能夠直接對應 發佈/訂閱模式(Publish/Subscribe)
行爲: 訂閱發送取消訂閱

說不如作,下面開始用代碼來更直觀的描繪下觀察者模式吧。
首先咱們定義一個發佈者 (至關於xxx公司)函數

let publisher = {
    
}

那麼一塊兒來按照訂閱郵件的過程想象下,發佈者具備那些屬性或者方法?網站

  • 首先,咱們訂閱一個xxx網站的郵件,是否是須要xxx網站給咱們提供訂閱入口?那麼publisher中一定會有一個方法提供給咱們實現訂閱
  • 其次,若是xxx公司要向訂閱者們發送本身的郵件,是否是須要一個方法去作?那麼publisher中一定會有一個方法提供給咱們實現發送或者說說發佈
  • 再而後,如例子所說若是我忽然不想訂閱xxx公司的郵件了,xxx公司 就得提供給我一個取消訂閱的入口,那麼publisher中一定會有一個方法提供給咱們實現取消訂閱
  • 最後,若是咱們訂閱了xxx公司的郵件,那麼他就得記錄我訂閱所用的郵箱地址吧,因此publisher中一定會有一個「註冊表」來存儲訂閱的對象,也就是說咱們的 郵箱地址

說到這裏一切都瞭然了,下面仍是講想象到的東西用代碼表達出來吧this

let publisher = {
    registration: {},
    subscribe: function (type, fn) {},
    unSubscribe: function (type, fnName) {},
    publish: function (type, message) {}
}

簡單解釋下,spa

  1. registration就是上面提到的註冊表,至於爲何把它設計成一個對象是由於考慮到xxx公司可能有更多類型的郵件,好比 遊戲,金融,投資理財等等,因此就把它設計成對象以key-value的形式存儲訂閱者, 好比:{'game':[],'monetary':[]}該形式
  2. subscribe 則是publisher提供給咱們的對其進行訂閱的方法,參數是typefn。type就是郵件的類型,fn就是咱們提供給publisher用於通知個人渠道 (郵箱)。在JavaScript中更多的是回調函數
  3. unSubscribepublisher提供給咱們的對其進行取消訂閱的方法,參數是typefnName。type就很少說了,fnName則是咱們提供給publisher用於取消訂閱的標誌,好比說郵箱,或者是回調函數的名字等等。
  4. publish說到比較重要的方法,這就是publisher向全部訂閱者發佈消息的方法。

下面開始一步一步得實現三個方法,registration保持不變:設計

首先是 subscribe
subscribe: function (type, fn) {
    if (Object.keys(this.registration).indexOf(type) >= 0) {
        this.registration[type].push(fn);
    } else {
        this.registration[type] = [];
        this.registration[type].push(fn);
    }
}

這裏的思路是將 Callback Function 存儲到registration對於類型的數組中,以待publish調用。code

而後是 unSubscribe
unSubscribe: function (type, fnName) {
    if (Object.keys(this.registration).indexOf(type) >= 0) {
        let index = -1;
        this.registration[type].forEach(function (func, idx) {
            if (func.name === fnName) {
                index = idx;
            }
        })
        index > -1 ? this.registration[type].splice(index, 1) : null
    }
}

思路是首先經過 type 肯定數組對象,而後經過方法對象的名字進行判斷,最後直接剔除操做。
** 這裏有個小知識點提一下:函數對象的name屬性就是該函數名 **

最後是 publish
publish: function (type, message) {
    if (Object.keys(this.registration).indexOf(type) >= 0) {
        for (let fn of this.registration[type]) {
            fn(message)
        }
    }
}

思路是經過 type 找到指定數組,而後對數組中的回調函數進行依次調用,達到發佈的目的。

寫到這裏,發佈者Publisher已經完成。那麼下面開始寫訂閱者Subscriber,如上面所說其實訂閱者就是一個 回調函數,例如:

let subscriber = function (param) {
    //do something
}

因此下面將整個代碼展現並演示下效果:

let publisher = {
    registration: {},
    subscribe: function (type, fn) {
        if (Object.keys(this.registration).indexOf(type) >= 0) {
            this.registration[type].push(fn);
        } else {
            this.registration[type] = [];
            this.registration[type].push(fn);
        }
    },
    unSubscribe: function (type, fnName) {
        if (Object.keys(this.registration).indexOf(type) >= 0) {
            let index = -1;
            this.registration[type].forEach(function (func, idx) {
                if (func.name === fnName) {
                    index = idx;
                }
            })
            index > -1 ? this.registration[type].splice(index, 1) : null
        }
    },
    publish: function (type, message) {
        if (Object.keys(this.registration).indexOf(type) >= 0) {
            for (let fn of this.registration[type]) {
                fn(message)
            }
        }
    }
}

let subscriberA = function (message) {
    console.log(`A收到通知:${message}`)
};

let subscriberB = function (message) {
    console.log(`B收到通知:${message}`)
};

let subscriberC = function (message) {
    console.log(`C收到通知:${message}`)
};

publisher.subscribe('game', subscriberA);
publisher.subscribe('game', subscriberB);
publisher.subscribe('game', subscriberC);

publisher.publish('game', '恭喜RNG得到LOL 2018季中賽冠軍!')

運行看下結果:

clipboard.png

結果如想象中同樣。
那再試一下取消訂閱,在 publish 以前加一段

publisher.unSubscribe('game', subscriberB.name)

再運行看下結果:

clipboard.png

咱們已經看到 訂閱者B 在取消訂閱後就沒再收到任何消息。

其實觀察者模式能作的東西還有不少,好比事件的監聽、狀態發生變化時的廣播等等。已經有過接觸的朋友均可能意識到這個模式特別靈活,在兩個角色之間正常通訊的同時也儘量得實現瞭解耦,給開發帶來極大的便利。其中有名的 Knockout 的核心之一就是觀察者模式,因此說觀察者模式在前端開發中起到了舉足輕重的做用。

源碼 在這,有興趣的朋友能夠看下

好了,寫到這裏本篇博客就結束了。有問題的朋友能夠在下方討論;若是文章有不足或者錯誤的地方,煩請你們多多指正。Thanks !!!

相關文章
相關標籤/搜索