javascript設計模式學習筆記之發佈-訂閱模式

發佈-訂閱模式

  • 定義對象間的一種 一對多 的依賴關係, 當一個對象的狀態發生改變時, 全部依賴於它的對象都將獲得通知
  • 簡單實現
// 定義發佈者
    var salesOffices = {};

    // 緩存列表, 存放訂閱者的回調函數
    salesOffices.clientList = [];
    
    // 定義訂閱者
    salesOffices.listen = function (fn) {
        this.clientList.push(fn);
    }

    // 發佈消息
    salesOffices.trigger = function () {
        for (var i = 0, fn; fn = this.clientList[i++];) {
            fn.apply(this, arguments)
        }
    }
    
    /*** 測試 ***/
    
    // 訂閱者1
    salesOffices.listen(function (price, squareMeter) {
        console.log('價格=' + price);
        console.log('squareMeter= ' + squareMeter);
    });

    // 訂閱者2
    salesOffices.listen(function (price, squareMeter) {
        console.log('價格=' + price);
        console.log('squareMeter= ' + squareMeter);
    });

    // 發佈消息
    salesOffices.trigger(2000, 80);
    salesOffices.trigger(3000, 100);
  • 上面的實現方式, 致使了, 每一個訂閱者都會收到發佈者發佈的消息
// 定義發佈者
    var salesOffices = {};

    // 緩存列表, 存放訂閱者的回調函數
    salesOffices.clientList = {};

    // 定義訂閱者 (增長緩存, 增長標示 key )
    salesOffices.listen = function (key, fn) {
        if (!this.clientList[key]) {
            this.clientList[key] = [];
        }
        this.clientList[key].push(fn);
    }

    // 發佈消息
    salesOffices.trigger = function () {

        var key = Array.prototype.shift.call(arguments),
            fns = this.clientList[key];

        if (!fns || fns.length === 0) {
            return false;
        }

        for (var i = 0, fn; fn = fns[i++];) {
            fn.apply(this, arguments)
        }
    }

提煉發佈-訂閱模式

// 核心事件
    var event = {
        clientList: {},
        listen: function (key, fn) {
            if (!this.clientList[key]) {
                this.clientList[key] = [];
            }
            this.clientList[key].push(fn);
        },
        trigger: function () {
            var key = Array.prototype.shift.call(arguments),
                fns = this.clientList[key];

            if (!fns || fns.length === 0) {
                return false
            }

            for (var i = 0, fn; fn = fns[i++];) {
                fn.apply(this, arguments);
            }
        }
    }
    
    // 取消訂閱消息
    event.remove = function (key, fn) {
        var fns = this.clientList[key];

        if (!fns) {
            return false;
        }

        if (!fn) {
            // 沒有傳入fn 取消key對應的全部的訂閱
            fns && (fns.length = 0);
        } else {
            // 傳入fn 刪除對應的fn
            for (var l = fns.length - 1; l >= 0; l--) {
                var _fn = fns[l];
                if (_fn === fn) {
                    fns.splice(l, 1)
                }
            }
        }
    }
    
    // 給任何對象動態增長髮布-訂閱功能
    var installEvent = function (obj) {
        for (var key in event) {
            if (event.hasOwnProperty(key)) {
                obj[key] = event[key];
            }
        }
    }
    
    /*** 測試 ***/
    var salesOffices = {};

    installEvent(salesOffices);

    // 訂閱者1
    salesOffices.listen('squareMeter80', function (price) {
        console.log('價格=' + price);
    });

    // 訂閱者2
    salesOffices.listen('squareMeter100', function (price) {
        console.log('價格=' + price);
    });

    // 發佈消息
    salesOffices.trigger('squareMeter80', 20000);
    salesOffices.trigger('squareMeter100', 30000);
  • 登陸案例
// 登陸成功, 發佈成功消息
    $.ajax('http://xxx.com/login', function (data) {
        login.trigger('loginSuccess', data);
    });

    // 這種寫法也很好
    var header = (function () {
        // 訂閱消息
        login.listen('loginSuccess', function (data) {
            header.setAvatar(data);
        })
        return {
            setAvatar: function (data) {
                console.log('header 模塊拿到用戶信息')
            }
        }
    })();
  • 以上寫法會有三個問題javascript

    • 須要一個相似'中介者'的角色, 把發佈者和訂閱者聯繫起來(經過 全局的 Event 來解決)
    • 以上必須先訂閱, 才能發佈
    • 全局命名的衝突

實現相似 Event 的發佈-訂閱模式

  • 優勢1: 時間上的解耦,
  • 優勢2: 對象之間的解耦
  • 缺點1: 建立訂閱者本生要消耗內存和時間
  • 缺點2: 弱化了對象之間的聯繫以後, 對象之間的必要聯繫也被埋沒
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(arr, fn) {
                    var ret;
                    for (var i = 0, l = arr.length; i < l; i++) {
                        var n = arr[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[eky]) {
                    if (fn) {
                        for (var i = cache[key].length - 1; 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 () {
                    return this.apply(_self, args);
                })
            };

            _create = function (namespace) {
                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, fn, 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;
    })();

觀察者(observer) 模式 和 發佈/訂閱模式 之間的區別

  • 本質上的區別及時在調度的地方不一樣
  • 分清楚誰是發佈者, 誰是觀察者, 誰是訂閱者
  • 觀察者模式
// subject(發佈) 中的目標發生變化. Observer(觀察) 能接受到變化
    function ObserverList() {
        this.observerList = [];
    }

    ObserverList.prototype.add = function (obj) {
        return this.observerList.push(obj);
    }
    
    ObserverList.prototype.get = function (index) {
        if (index > -1 && this.observerList.length) {
            return this.observerList[index];
        }
    }

    ObserverList.prototype.indexOf = function (obj, startIndex) {
        var i = startIndex;

        while(i < this.observerList.length) {
            if (this.observerList[i] === obj) {
                return i;
            }
            i++;
        }

        return -1;
    }
    
    ObserverList.prototype.removeAt = function (index) {
        this.observerList.splice(index, 1);
    }
    
    // 發佈者
    function Subject() {
        this.observers = new ObserverList();
    }

    Subject.prototype.addObserver = function (observer) {
        this.observers.add(observer);
    }

    Subject.prototype.removeObserver = function (observer) {
        this.observers.removeAt(this.observers.indexOf(observer, 0));
    }

    Subject.prototype.notify = function (context) {
        var observerCount = this.observers.count();
        for (var i = 0; i < observerCount; i++) {
            this.observers.get(i).update(context);
        }
    }

    // 觀察者
    function Observer() {
        this.update = function () {
            // ...
        }
    }
  • 發佈(Publish)/訂閱(Subscribe)模式
var pubsub = {};
    (function (myObject) {
        var topics = {};

        var subUid = -1;

        // 發佈
        myObject.publish = function (topic, args) {
            if (!topics[topic]) {
                return false;
            }

            var subscribers = topics[topic],
                len = subscribers ? subscribers.length : 0;

            while (len--) {
                subscribers[len].func(topic, args);
            }

            return this;
        }

        // 訂閱者
        myObject.subscribe = function (topic, func) {
            if (!topics[topic]) {
                topics[topic] = [];
            }

            var token = (++subUid).toString();

            topics[topic].push({
                token: token,
                func: func
            })
        }

        // 取消訂閱
        myObject.unsubscribe = function (token) {
            for (var m in topics) {
                if (topics[m]) {
                    for (var i = 0, j = topics[m].length; i < j; i++) {
                        if (topics[m][i].token === token) {
                            topics[m].splice(i, 1);
                            return token;
                        }
                    }
                }
            }
            return this;
        }
    })(pubsub)
相關文章
相關標籤/搜索