JavaScript設計模式與開發實踐 - 觀察者模式

概述

觀察者模式又叫發佈 - 訂閱模式(Publish/Subscribe),它定義了一種一對多的關係,讓多個觀察者對象同時監聽某一個目標對象(爲了方便理解,如下將觀察者對象叫作訂閱者,將目標對象叫作發佈者)。發佈者的狀態發生變化時就會通知全部的訂閱者,使得它們可以自動更新本身。javascript

觀察者模式的使用場合就是:當一個對象的改變須要同時改變其它對象,而且它不知道具體有多少對象須要改變的時候,就應該考慮使用觀察者模式。html

觀察者模式的中心思想就是促進鬆散耦合,一爲時間上的解耦,二爲對象之間的解耦。讓耦合的雙方都依賴於抽象,而不是依賴於具體,從而使得各自的變化都不會影響到另外一邊的變化。java

實現

(function (window, undefined) {
    var _subscribe = null,
        _publish = null,
        _unsubscribe = null,
        _shift = Array.prototype.shift, // 刪除數組的第一個 元素,並返回這個元素
        _unshift = Array.prototype.unshift, // 在數組的開頭添加一個或者多個元素,並返回數組新的length值
        namespaceCache = {},
        _create = null,
        each = function (ary, fn) {
            var ret = null;
            for (var i = 0, len = ary.length; i < len; i++) {
                var n = ary[i];
                ret = fn.call(n, i, n);
            }
            return ret;
        };

    // 訂閱消息
    _subscribe = function (key, fn, cache) {
        if (!cache[key]) {
            cache[key] = [];
        }
        cache[key].push(fn);
    };

    // 取消訂閱(取消所有或者指定消息)
    _unsubscribe = 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] = [];
            }
        }
    };

    // 發佈消息
    _publish = function () {
        var cache = _shift.call(arguments),
            key = _shift.call(arguments),
            args = arguments,
            _self = this,
            ret = null,
            stack = cache[key];

        if (!stack || !stack.length) {
            return;
        }

        return each(stack, function () {
            return this.apply(_self, args);
        });
    };

    // 建立命名空間
    _create = function (namespace) {
        var namespace = namespace || "default";
        var cache = {},
            offlineStack = {},    // 離線事件,用於先發布後訂閱,只執行一次
            ret = {
                subscribe: function (key, fn, last) {
                    _subscribe(key, fn, cache);
                    if (!offlineStack[key]) {
                        offlineStack[key] = null;
                        return;
                    }
                    if (last === "last") { // 指定執行離線隊列的最後一個函數,執行完成以後刪除
                        offlineStack[key].length && offlineStack[key].pop()();  // [].pop => 刪除一個數組中的最後的一個元素,而且返回這個元素
                    } else {
                        each(offlineStack[key], function () {
                            this();
                        });
                    }
                    offlineStack[key] = null;
                },
                one: function (key, fn, last) {
                    _unsubscribe(key, cache);
                    this.subscribe(key, fn, last);
                },
                unsubscribe: function (key, fn) {
                    _unsubscribe(key, cache, fn);
                },
                publish: function () {
                    var fn = null,
                        args = null,
                        key = _shift.call(arguments),
                        _self = this;

                    _unshift.call(arguments, cache, key);
                    args = arguments;
                    fn = function () {
                        return _publish.apply(_self, args);
                    };

                    if (offlineStack && offlineStack[key] === undefined) {
                        offlineStack[key] = [];
                        return offlineStack[key].push(fn);
                    }
                    return fn();
                }
            };

        return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret;
    };

    window.pubsub = {
        create: _create, // 建立命名空間
        one: function (key, fn, last) { // 訂閱消息,只能單一對象訂閱
            var pubsub = this.create();
            pubsub.one(key, fn, last);
        },
        subscribe: function (key, fn, last) { // 訂閱消息,可多對象同時訂閱
            var pubsub = this.create();
            pubsub.subscribe(key, fn, last);
        },
        unsubscribe: function (key, fn) { // 取消訂閱,(取消所有或指定消息)
            var pubsub = this.create();
            pubsub.unsubscribe(key, fn);
        },
        publish: function () { // 發佈消息
            var pubsub = this.create();
            pubsub.publish.apply(this, arguments);
        }
    };
})(window, undefined);

應用

假如咱們正在開發一個商城網站,網站裏有header頭部、nav導航、消息列表、購物車等模塊。這幾個模塊的渲染有一個共同的前提條件,就是必須先用ajax異步請求獲取用戶的登陸信息。ajax

至於ajax請求何時能成功返回用戶信息,這點咱們沒有辦法肯定。更重要的一點是,咱們不知道除了header頭部、nav導航、消息列表、購物車以外,未來還有哪些模塊須要使用這些用戶信息。若是它們和用戶信息模塊產生了強耦合,好比下面這樣的形式:編程

login.succ(function (data) {
    header.setAvatar(data.avatar); // 設置header模塊的頭像
    nav.setAvatar(data.avatar); // 設置導航模塊的頭像
    message.refresh(); // 刷新消息列表
    cart.refresh(); // 刷新購物車列表
});

如今登陸模塊是由你負責編寫的,但咱們還必須瞭解header模塊裏設置頭像的方法叫setAvatar、購物車模塊裏刷新的方法叫refresh,這種耦合性會使程序變得僵硬,header模塊不能隨意再改變setAvatar的方法名。這是針對具體實現編程的典型例子,針對具體實現編程是不被贊同的。設計模式

等到有一天,項目中又新增了一個收貨地址管理的模塊,這個模塊是由另外一個同事所寫的,此時他就必須找到你,讓你登陸以後刷新一下收貨地址列表。因而你又翻開你3個月前寫的登陸模塊,在最後部分加上這行代碼:數組

login.succ(function (data) {
    header.setAvatar(data.avatar);
    nav.setAvatar(data.avatar);
    message.refresh();
    cart.refresh();
    address.refresh(); // 增長這行代碼
});

咱們就會愈來愈疲於應付這些突如其來的業務要求,不停地重構這些代碼。app

用觀察者模式重寫以後,對用戶信息感興趣的業務模塊將自行訂閱登陸成功的消息事件。當登陸成功時,登陸模塊只須要發佈登陸成功的消息,而業務方接受到消息以後,就會開始進行各自的業務處理,登陸模塊並不關心業務方究竟要作什麼,也不想去了解它們的內部細節。改善後的代碼以下:異步

$.ajax('http:// xxx.com?login', function(data) { // 登陸成功
    pubsub.publish('loginSucc', data); // 發佈登陸成功的消息
});

// 各模塊監聽登陸成功的消息:

var header = (function () { // header模塊
    pubsub.subscribe('loginSucc', function(data) {
        header.setAvatar(data.avatar);
    });
    return {
        setAvatar: function(data){
            console.log('設置header模塊的頭像');
        }
    };
})();

var nav = (function () { // nav模塊
    pubsub.subscribe('loginSucc', function(data) {
        nav.setAvatar(data.avatar);
    });
    return {
        setAvatar: function(avatar) {
            console.log('設置nav模塊的頭像');
        }
    };
})();

如上所述,咱們隨時能夠把setAvatar的方法名改爲setTouxiang。若是有一天在登陸完成以後,又增長一個刷新收貨地址列表的行爲,那麼只要在收貨地址模塊里加上監聽消息的方法便可,而這可讓開發該模塊的同事本身完成,你做爲登陸模塊的開發者,永遠不用再關心這些行爲了。代碼以下:函數

var address = (function () { // 地址模塊
    pubsub.subscribe('loginSucc', function(obj) {
        address.refresh(obj);
    });
    return {
        refresh: function(avatar) {
            console.log('刷新收貨地址列表');
        }
    };
})();

優缺點

優勢

  1. 支持簡單的廣播通訊,自動通知全部已經訂閱過的對象;

  2. 頁面載入後發佈者很容易與訂閱者存在一種動態關聯,增長了靈活性;

  3. 發佈者與訂閱者之間的抽象耦合關係可以單獨擴展以及重用。

缺點

  1. 建立訂閱者自己要消耗必定的時間和內存,並且當你訂閱一個消息後,也許此消息最後都未發生,但這個訂閱者會始終存在於內存中;

  2. 雖然能夠弱化對象之間的聯繫,但若是過分使用的話,對象和對象之間的必要聯繫也將被深埋在背後,會致使程序難以跟蹤維護和理解。

參考

相關文章
相關標籤/搜索