javascript中的設計模式之釋出-訂閱模式

泛舟青煙發表於2020-07-22

一、定義

  又叫觀察者模式,他定義物件間的依照那個一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將的到通知。在javascript中,我們一般用時間模型來替代傳統的釋出-訂閱模式

二、舉例

  js中對dom元素繫結事件監聽,就是簡單的釋出-訂閱。另外在很多的框架和外掛中都會存在使用這種方式來,比如vue的watch

三、結構

  釋出-訂閱模式可以分為兩種形式,一種是釋出者和訂閱者直接進行通訊,其結構如下:

 

  另一種是通過中介進行通訊,釋出者和訂閱者互不相知,其結構如下:

四、實現

1.釋出者和訂閱者直接進行通訊

  這種模式的核心在於,要在釋出者中維護儲存一個訂閱者的回撥函式的陣列。

  典型的例子就是繫結dom元素,但是js未將對應的釋出者暴露,這是瀏覽器實現的,因此我們使用的都是直接進行訂閱。我們可以自定義一個事件,程式碼如下:

// 釋出者
var publisher = {
    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, l = fns.length; i < l; i++){
            fns[i].apply(this, arguments);
        }
    },
    run: function(){
        // 釋出者根據實際情況在合適時機發布事件
        this.trigger("start_load", "開始載入");
        console.log("start load");
        this.trigger("loading", "正在載入");
        console.log("loading...");
        this.trigger("finish_load", "載入完成");
        console.log("finish load");
    }
};

// 訂閱者
var subscriber = {
    init: function(){
        // 訂閱
        publisher.listen("finish_load", function(rst){
            console.log("我是訂閱者,我訂閱了釋出者的載入完成事件,現在我收到了釋出者的資訊:" + rst);
        });
    }
};

subscriber.init();

publisher.run();

  這種的模式很簡單,但是他的缺點在於如果有多個釋出者,那麼就需要讓每個釋出者維護listen、trigger函式和一個事件回撥函式快取列表,比如我們可以會對js檔案的載入過程進行訂閱,也可能會對dom的構建過程進行訂閱等等,顯然每個釋出者分別建立一個物件是耗費記憶體也是不優雅的。另外這種模式存在著釋出者和訂閱這的耦合性,往往在開發過程中,我們可能根本沒有必要讓釋出者和訂閱者進行通訊,各自做好自己的事情就好了。因此這種方式很少會用到。

2.通過中介進行通訊

  針對上面的方式的缺點,就有了這種方式,這樣的模式是一種全域性的釋出-訂閱模式。其核心是建立一箇中介,也就是一個全域性的Event物件,讓他來幫助釋出者和訂閱者溝通。程式碼如下:

// 事件物件,作為中介
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, l = fns.length; i < l; i++){
            fns[i].apply(this, arguments);
        }
    }
};
// 釋出者
var publisher = {
    run: function(){
        Event.trigger("start_load", "開始載入");
        console.log("start load");
        Event.trigger("loading", "正在載入");
        console.log("loading...");
        Event.trigger("finish_load", "載入完成");
        console.log("finish load");
    }
};

// 訂閱者
var subscriber = {
    init: function(){
        // 訂閱
        Event.listen("finish_load", function(rst){
            console.log("我是訂閱者,我訂閱了釋出者的載入完成事件,現在我收到了釋出者的資訊:" + rst);
        });
    }
};
subscriber.init();
publisher.run();

  我們看繪製二維地圖的leaflet框架中,對於釋出-訂閱模式的實現:

export var Events = {
    // 新增監聽事件,types:{mouseclick: fn, dbclick: fn} 或者:"mouseclick dbclick"
    on: function (types, fn, context) {...},
    // 移除事件,若未設定任何引數,則刪除該物件所有的事件。若fn未設定,則刪除物件中所有的type事件
    off: function (types, fn, context) {...},

    // 內部註冊監聽事件
    _on: function (type, fn, context) {
        this._events = this._events || {};
        var typeListeners = this._events[type];    // 獲取物件中其他註冊的該事件的回撥函式
        // 若物件中未曾設定過相同事件名稱,則儲存其回撥函式
        if (!typeListeners) {
            typeListeners = [];
            this._events[type] = typeListeners;
        }

        var newListener = {fn: fn, ctx: context};
        ...
        typeListeners.push(newListener);
    },

    // 移除事件, 若未設定fn則刪除所有的type事件。,否則刪除對應的事件
    _off: function (type, fn, context) {...},

    // 觸發物件中的所有type事件,若設定了propagate則觸發父物件的type事件
    fire: function (type, data, propagate) {
        if (!this.listens(type, propagate)) { return this; }   // 檢查是否註冊了type事件
        // 構建回撥函式中引數事件物件
        var event = Util.extend({}, data, {
            type: type,
            target: this,
            sourceTarget: data && data.sourceTarget || this
        });
        if (this._events) {
            var listeners = this._events[type];

            // _firingCount用於防止觸發事件未執行完成同時刪除該事件。
            // _firingCound表示正在執行的回撥函式的個數,當為0時表示沒有正在執行的事件。可以直接刪除,否則需要將_events進行復制,防止刪除掉需要回撥的物件
            if (listeners) {
                this._firingCount = (this._firingCount + 1) || 1;
                // 執行物件註冊的所有該事件的回撥函式
                for (var i = 0, len = listeners.length; i < len; i++) {
                    var l = listeners[i];
                    l.fn.call(l.ctx || this, event);
                }
                this._firingCount--;
            }
        }
        return this;
    },

    listens: function (type) {
        var listeners = this._events && this._events[type];
        return !!(listeners && listeners.length);
    },
};
export var Evented = Class.extend(Events);

  使用Event基礎類用來實現釋出-訂閱,而這個基礎類類似於一個介面,他需要依附於一個實際的物件來構造該物件的事件系統,比如框架中的圖層需要有各種滑鼠、鍵盤觸控事件,因此為了模仿類似dom一樣的觸發方式,圖層類就要繼承這個Event類:

 

 

 

 

  因此在訂閱的時候,就可以直接:

layer.on("click",function(){});

五、總結

釋出-訂閱模式的關鍵在於用一個事件物件對其進行實現,其中需要有以下幾點:

  1.快取物件:用於儲存訂閱者監聽的回撥函式,鍵為事件名稱,值為該事件名下所有的回撥函式,其結構形如:

var cache = {
    "click": [fn1,fn2,fn3...],
    "dbclick": [fn4,fn5,fn6...]
        ...
}

  2.listen/on函式:監聽函式,為訂閱者使用,通常包含兩個引數:事件名稱和回撥函式,內部會將這兩個引數儲存到快取物件中

  3.trigger/fire函式:釋出函式,為釋出者使用,通常包含一個引數:事件名稱。內部通過事件名稱到快取物件中查詢對應的回撥函式陣列,並依次執行

  4.remoe/off函式:接觸監聽函式,為訂閱者使用,通常包含一個或兩個引數:事件名稱或註冊監聽時的回撥函式。若為一個引數事件名稱,則會到快取函式中查詢到對應的註冊的回撥函式的陣列,並將其清空。若有第二個引數回撥函式,會在快取物件中找到事件名稱對應的回撥函式陣列,查詢是否存在引數中的回撥函式,有的話,則只刪除這一個回撥函式

相關文章