跳到主要內容
黯羽輕揚每天積累一點點

觀察者模式_JavaScript 設計模式 4

免費2015-07-16#JS#Design_Pattern#JavaScript观察者模式#Observer_Pattern#发布订阅模式#Publish/Subscribe

最經典的觀察者模式應用就是事件機制了,本文詳細介紹用 JS 如何實現觀察者模式和與之類似的發布訂閱模式

##一.Observer(觀察者)模式

利用觀察者模式可以輕易地建立物件之間「一對多」的依賴關係; 利用觀察者模式的機制可以很容易的實現這種依賴關係的動態維護

(引自 黯羽輕揚:設計模式之觀察者模式(Observer Pattern)

觀察者模式解耦了主題和觀察者,主題不清楚觀察者的細節,只知道所有觀察者都有 update 介面;觀察者對主題知道的更少,只能被動等待主題更新;各觀察者之間根本看不到彼此,更不知道沒有沒有關注同一主題了

松耦合意味著靈活性(彈性),只要保證實現既定介面,就可以獨立修改主題、觀察者,而不會影響二者的交互關係

##二.經典觀察者模式

觀察者模式概念比較簡單,經典實現需要增加一級抽象(Observ、Subject 的介面定義), JS 實現的簡單觀察者模式如下:

/* 經典觀察者模式
 * Observer、Subject
 * 也可以增加1級抽象,不過JS繼承太麻煩,此處從簡
 * 手動維護List
 */

// Subject
function Subject(name) {
    this.name = name;
    this.list = [];
}
// 關注
Subject.prototype.regist = function(obj) {
    this.list.push(obj);
};
// 取消關注
Subject.prototype.unregist = function(obj) {
    var list = this.list;

    for(var i = 0, len = list.length; i < len; i++) {
        if (list[i] === obj) {
            list.splice(i, 1);
        }
    }
};
// 通知所有粉絲
Subject.prototype.notify = function() {
    var list = this.list;

    for(var i = 0, len = list.length; i < len; i++) {
        list[i].update({data: this.name});
    }
};

// Observer
function Observer(id) {
    this.id = id;
    // 供Subject調用的更新方法
    this.update = function(dataObj) {
        // ...
        console.log(this.id + ' received ' + dataObj.data + '\'s update');
    };
}

// test
var subject = new Subject('My Topic');
var observer1 =  new Observer(1);
var observer2 =  new Observer(2);
var observer3 =  new Observer(3);

subject.regist(observer1);
subject.regist(observer2);
subject.notify();   // 主題更新,通知所有觀察者

subject.regist(observer3);  // 新加入observer3
subject.notify();

// observer1不玩了
subject.unregist(observer1);
subject.notify();

執行結果如下:

[caption id="attachment_663" align="alignnone" width="239"]觀察者模式 觀察者模式[/caption]

P.S. 如果對 1 級抽象感興趣,可以參考 黯羽輕揚:重新理解 JS 的 6 種繼承方式 自行實現 1 級抽象

##三.Publish/Subscribe(發布/訂閱)模式

觀察者模式分離了主題和觀察者,交互關係是嵌在二者內部的(必須實現既定介面),如果把交互關係再解耦出來的話,那就叫發布訂閱模式

發布訂閱模式在主題和觀察者之間新增了一層事件機制,由事件機制為雙方提供介面,維持交互關係,多這樣一層的好處是可以引入事件隊列、事件冒泡(不止 DOM 中可以有喔~)等等更多的控制

到這裡就已經差不多把解耦進行到底了,帶來巨大靈活性的同時,也存在一些不可避免的缺點,比如如果有一個觀察者崩潰了,因為解耦的關係,這個消息無法傳遞出去,主題不知道,周圍的觀察者也不知道,其他依賴於這個觀察者的模組也不會知道。(雖然理論上透過增強事件管理機制可以對這種情況加以控制,但這會使事件層變得越來越複雜,結構也越來越臃腫)

##四.自定義事件機制實現發布/訂閱模式

注意是「事件機制」,而不是「事件」,因為自定義事件是 DOM API,只能綁定在 DOM 元素上,具體請查看 Document.createEvent() - Web API Interfaces | MDN

所以必須自定義「事件機制」,實現的具體細節請查看 黯羽輕揚:JS 學習筆記 11_高級技巧 底部

DOM3 級 API 支援建立自定義事件,具體如下:

// 建立自定義事件物件
var event = document.createEvent('CustomEvent');
// 初始化事件
event.initCustomEvent('myeve', true, true);
// initCustomEvent 介面定義如下
/*
void initCustomEvent(
    in DOMString type,
    in boolean canBubble,
    in boolean cancelable,
    in any detail
);
*/

document.createEventinitCustomEvent 都已經過時了,現在可以直接用構造函數建立自定義事件(例子請查看 JavaScript CustomEvent )物件。當然目前(2015-7-13)過時的東西相容性是最好的,詳情見 Document.createEvent() - Web API Interfaces | MDN

如果需要相容 [IE8-],就必須採取一些其他的手段,此處不展開,請查看 JavaScript 自定義事件

P.S. 如果專案引入了主流 JS 庫(框架),那麼一般都支援自定義事件,但也都是與 DOM 元素綁定的,非 DOM 元素物件的事件機制要自行實現,有一個極簡版的發布/訂閱實現,可以參考之 addyosmani/pubsubz ,程式碼很精巧,值得一看

##五.觀察者模式具體應用

觀察者模式很重要的一個應用場景是 Ajax 請求的回調邏輯,主題是 Ajax 請求傳回的數據,觀察者是回調函式中的各個邏輯塊(list 能夠保證執行順序),只需要在回調函式中 notify 所有觀察者即可,示例程式碼如下:

// 發布/訂閱模式的實現來自: https://github.com/addyosmani/pubsubz
(function(window) {
    var topics = {},
        subUid = -1,
        pubsubz = {};

    pubsubz.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 true;
    };

    pubsubz.subscribe = function(topic, func) {
        if (!topics[topic]) {
            topics[topic] = [];
        }

        var token = (++subUid).toString();
        topics[topic].push({
            token: token,
            func: func
        });

        return token;
    };

    pubsubz.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 false;
    };

    window.pubsubz = pubsubz;
}(this));


// 具體應用
var psz = pubsubz;
var eventName = 'DataUpdate';

var obs1 = psz.subscribe(eventName, function(data) {
    console.log('obs1 received ' + data);
    console.log('清除無效數據');
});
var obs2 = psz.subscribe(eventName, function(data) {
    console.log('obs2 received ' + data);
    console.log('顯示新數據');
});
var obs3 = psz.subscribe(eventName, function(data) {
    console.log('obs3 received ' + data);
    console.log('更新圖表列表');
});

// 模擬ajax
function ajax(url, callback) {
    // 50ms後執行回調(假設已經拿到了數據)
    setTimeout(function(data) {
        data = 'data';  // 模擬數據

        callback(data);
    }, 50);
}

ajax('emptyUrl', function(data) {
    psz.publish(eventName, data);
});

// 執行結果如下:
// obs3 received DataUpdate
// 更新圖表列表
// obs2 received DataUpdate
// 顯示新數據
// obs1 received DataUpdate
// 清除無效數據
// (逆序是因為pubsubz.publish採用了遞減迭代)

當然,這樣做也會帶來一定的問題,比如邏輯分散在各個觀察者上,會導致編碼難度增加(類似於事件驅動的缺點),使用時需要根據具體場景權衡

###參考資料

  • 《JavaScript 設計模式》

  • 相關博文若干

評論

暫無評論,快來發表你的看法吧

提交評論