對釋出-訂閱者模式的解析

DUTX發表於2019-02-18

介紹

在使用釋出-訂閱者模式之前,先了解什麼是釋出-訂閱者模式。釋出訂閱者模式是一種一對多的依賴關係。多個物件(訂閱者:subscriber)同時監聽同一物件(釋出者:publisher)的資料狀態變化。當釋出者的資料狀態發生變化的時候,就會通知所有的訂閱者,同時還可能以事件物件的形式傳遞一些訊息。在生活中就有很多這樣的例子,比如微信公眾號的釋出文章與使用者的訂閱公眾號,報紙的發刊以及每家每戶的訂閱報紙等等,這些都是對釋出訂閱者模式很好的應用。

這樣,我們就能知曉釋出者以及訂閱者包含的內容了,概括如下:

釋出者(publisher):

  • 一個物件 subscribers:{type:[subscriberFn,...]} 包了含訂閱的型別及其相關訂閱者(由於訂閱的事件可能多種所以此處需新增一個訂閱型別)
  • 一個函式 subscribe(newSubscriberFn,type) 新增新的訂閱者到對應訂閱型別的訂閱者陣列中
  • 一個函式 unsubscribe(subscriberFn,type) 從陣列中移除指定訂閱者
  • 一個函式 publish(type) 當釋出者的某一狀態改變是,通知所有的訂閱者,即遍歷訂閱者提供的方法

訂閱者(subscriber):

  • 一個函式 subscriberFn() 當釋出者狀態發生改變時通知所使用的的回撥函式

釋出訂閱的整個過程就如下所示:

對釋出-訂閱者模式的解析

理論的東西弄明白了,下面我們就進行具體的應用。

栗子

場景:某報社釋出的報紙分為日報和月報。小王訂閱了這家報社的日報,小李訂閱了這家報紙的月報,小趙兩類的報紙都訂閱了。當報社有新的報紙出版了,小王、小李以及小趙都能閱讀到他們訂閱的報紙。

首先定義報社這個物件:

const PaperPublisher = {
	subscribers: {
		dailyPaper: [],
		monthlyPaper: [],
		all: []
	},
	subscribe (newSubscriberFn, type) {
		let type = type ? type : 'all';
		this.subscribers[type].push(newSubscriberFn);
	},
	unsubscribe (subscriberFn, type) {
		let type = type ? type : 'all',
			index = 0;
		for (let i = 0, len = this.subscribers[type].length; i < len; i++) {
			if (this.subscribers[type][i] == subscriberFn) {
				index = i;
				break;
			}
		}
		this.subscribers[type].splice(index,1);
	},
	publish (type,value) {
		for (let i = 0, len = this.subscribers[type].length; i < len; i++) {
			this.subscribers[type][i](value);
		}
		for (let i = 0, len = this.subscribers.all.length; i < len; i++) {
			this.subscribers.all[i](value);
		}
	},
	publishDailyPaper () {  // 釋出日報
		this.publish('dailyPaper','日報的內容');
	},
	publishMonthlyPaper () {  // 釋出月報
		this.publish('monthlyPaper','月報的內容');
	}
}
複製程式碼

然後分別定義小王、小李、小趙

// 小王
const XiaoWangSubscriber = {
	subscriberFn (value) {
		console.log(`我是小王,正在讀 ${value}`);
	}
}
// 小李
const XiaoLiSubscriber = {
	subscriberFn (value) {
        console.log(`我是小李,正在讀 ${value}`);
	}
}
// 小趙
const XiaoZhaoSubscriber = {
	subscriberFn (value) {
		console.log(`我是小趙,正在讀 ${value}`);
	}
}
複製程式碼

接下來,讓小王訂閱日報,小李訂閱月報,小趙兩類的報紙都訂閱

PaperPublisher.subscrib(XiaoWangSubscriber.subscriberFn,'dailyPaper');
PaperPublisher.subscrib(XiaoLiSubscriber.subscriberFn,'monthlyPaper');
PaperPublisher.subscrib(XiaoZhaoSubscriber.subscriberFn);
複製程式碼

當報社釋出日報,小王和小趙能收到報紙並閱讀

PaperPublisher.publishDailyPaper();
 // 我是小王,正在讀 日報的內容
 // 我是小趙,正在讀 日報的內容
複製程式碼

當報社釋出月報,小李和小趙能收到報紙並閱讀

PaperPublisher.publishMonthlyPaper();
 // 我是小李,正在讀 月報的內容
 // 我是小趙,正在讀 月報的內容
複製程式碼

小王因為工作原因,去外地出差了,於是取消了日報的訂閱

PaperPublisher.unsubscribe(XiaoWangSubscriber.subscriberFn,'dailyPaper');
複製程式碼

以上就是整個栗子的全部程式碼了。

進一步提升

其實所有的釋出者,前四點屬性和方法都是必備的,所以我們能夠提取出這些屬性和方法存放在一個公用的物件中。在之後的使用中,我們就可以將它們複製到任何一個物件中,將這些物件轉換為訂閱者。特別的在函式unsubscribe和publish中都存在遍歷subscribers中的資料的操作,所以在公用的物件中,定義了一個visitSubscribers()方法。下面就是對公用物件的定義:

var publisher = {
    subscribers: {
        all: []
    },
    subscribe: function (fn, type) {
        let type = type || 'all';
        if (typeof this.subscribers[type] === "undefined") {
            this.subscribers[type] = [];
        }
        this.subscribers[type].push(fn);
    },
    unsubscribe: function (fn, type) {
        this.visitSubscribers('unsubscribe', fn, type);
    },
    publish: function (type, publication) {
        this.visitSubscribers('publish', publication, type);
    },
    visitSubscribers: function (action, arg, type) {
        let pubtype = type || 'all',
            subscribers = this.subscribers[pubtype],
            i,
            max = subscribers.length;

        for (i = 0; i < max; i += 1) {
            if (action === 'publish') {
                subscribers[i](arg);
            } else {
                if (subscribers[i] === arg) {
                    subscribers.splice(i, 1);
                }
            }
        }
    }
};
複製程式碼

除了公用的物件接著還需要定義一個函式來將一個普通的物件包裝成一個釋出者:

function makePublisher(o) {
    var i;
    for (i in publisher) {
        if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") {
            o[i] = publisher[i];
        }
    }
    o.subscribers = {any: []};
}
複製程式碼

接下來,我們就可以以另外一種方式定義PaperPublisher物件了:

// PaperPublisher物件本身包括髮布日報和釋出週報的兩個方法
var PaperPublisher = {
    publishDailyPaper () {  // 釋出日報
		this.publish('dailyPaper','日報的內容');
	},
	publishMonthlyPaper () {  // 釋出月報
		this.publish('monthlyPaper','月報的內容');
	}
};
// 將物件包裝成釋出者
makePublisher(PaperPublisher);
複製程式碼

釋出-訂閱者模式的優缺點

優點:

  • 實現時間上的解耦(元件,模組之間的非同步通訊)
  • 物件之間的解耦,交由釋出訂閱的物件管理物件之間的耦合關係.

缺點:

  • 建立訂閱者本身會消耗記憶體,訂閱訊息後,也許,永遠也不會有釋出,而訂閱者始終存在記憶體中.
  • 物件之間解耦的同時,他們的關係也會被深埋在程式碼背後,這會造成一定的維護成本.

相關文章