簡介
釋出-訂閱模式又叫做觀察者模式,他定義了一種一對多的依賴關係,即當一個物件的狀態發生改變的時候,所有依賴他的物件都會得到通知。
回憶曾經
作為一名前端開發人員,給DOM節點繫結事件可是再頻繁不過的事情。比如如下程式碼
document.body.addEventListener('click',function () {
alert(2333);
},false);
document.body.click();//模擬點選事件
複製程式碼
這裡我們訂閱了document.body的click事件,當body被點選的時候,他就向訂閱者釋出這個訊息,彈出2333.我們也可以隨意的增加和刪除訂閱者,當訊息一發布,所有的訂閱者都會收到訊息。
document.body.addEventListener('click',function () {
alert(11111);
},false);
document.body.addEventListener('click',function () {
alert(222);
},false);
document.body.addEventListener('click',function () {
alert(333);
},false);
document.body.click();//模擬點選事件
複製程式碼
值得注意的是,手動觸發事件這裡我們直接用了document.body.click();但是更好的做法是IE下用fireEvent,標準瀏覽器下用dispatchEvent,如下:
let fireEvent = function (element,event) {
if (document.createEventObject) {
var evt = document.createEventObject();
return element.fireEvent('on'+event,evt);
}else{
var evt = document.createEvent('HTMLEvents');
evt.initEvent(event,true,true);
return element.dispatchEvent(evt);
}
}
document.addEventListener('shout',function (event) {
alert('shout');
})
fireEvent(document,'shout');
複製程式碼
暢談現在
人的日常生活離不開各種人際交涉,比如你的朋友有很多,這時候你要結婚了,要以你為釋出者,開啟你的通訊錄,挨個打電話通知各個訂閱者你要結婚的訊息。抽象一下,實現釋出-訂閱模式需要:
- 釋出者(你)
- 快取列表(通訊錄,你的朋友們相當於訂閱了你的所有訊息)
- 釋出訊息的時候遍歷快取列表,依次觸發裡面存放的訂閱者的回撥函式(挨個打電話)
- 另外,回撥函式中還可以新增很多引數,,訂閱者可以接收這些引數,比如你會告訴他們婚禮時間,地點等,訂閱者收到訊息後可以進行各自的處理。
let yourMsg = {};
yourMsg.peopleList = [];
yourMsg.listen = function (fn) {
this.peopleList.push(fn);
}
yourMsg.triger = function () {
for(var i = 0,fn;fn=this.peopleList[i++];){
fn.apply(this,arguments);
}
}
yourMsg.listen(function (name) {
console.log(`${name}收到了你的訊息`);
})
yourMsg.listen(function (name) {
console.log('哈哈');
})
yourMsg.triger('張三');
yourMsg.triger('李四');
複製程式碼
- 以上就是一個簡單的釋出-訂閱的實現,但是我們會發現訂閱者會收到釋出者釋出的每一條資訊,如果李四比較陰暗,不想聽到你結婚的訊息,只想聽到你的壞訊息,比如你被開除了,他就心裡高興。這時候我們就需要加一個key,讓訂閱者只訂閱自己感興趣的訊息。
let yourMsg = {};
yourMsg.peopleList ={};
yourMsg.listen = function (key,fn) {
if (!this.peopleList[key]) { //如果沒有訂閱過此類訊息,建立一個快取列表
this.peopleList[key] = [];
}
this.peopleList[key].push(fn);
}
yourMsg.triger = function () {
let key = Array.prototype.shift.call(arguments);
let fns = this.peopleList[key];
if (!fns || fns.length == 0) {//沒有訂閱 則返回
return false;
}
for(var i=0,fn;fn=fns[i++];){
fn.apply(this,arguments);
}
}
yourMsg.listen('marrgie',function (name) {
console.log(`${name}想知道你結婚`);
})
yourMsg.listen('unemployment',function (name) {
console.log(`${name}想知道你失業`);
})
yourMsg.triger('marrgie','張三');
yourMsg.triger('unemployment','李四');
複製程式碼
- 你需要釋出訊息,同樣的所有的人都有朋友圈,也都需要釋出訊息,因此我們有必要把釋出-訂閱的功能提取出來,放在一個單獨的物件內,誰需要誰去動態安裝釋出-訂閱功能(installEvent函式實現了動態安裝釋出-訂閱功能)。
var event = {
peopleList:[],
listen:function (key,fn) {
if (!this.peopleList[key]) { //如果沒有訂閱過此類訊息,建立一個快取列表
this.peopleList[key] = [];
}
this.peopleList[key].push(fn)
},
trigger:function () {
let key = Array.prototype.shift.call(arguments);
let fns = this.peopleList[key];
if (!fns || fns.length == 0) {//沒有訂閱 則返回
return false;
}
for(var i=0,fn;fn=fns[i++];){
fn.apply(this,arguments);
}
}
}
var installEvent = function (obj) {
for(var i in event){
obj[i] = event[i];
}
}
let yourMsg = {};
installEvent(yourMsg);
yourMsg.listen('marrgie',function (name) {
console.log(`${name}想知道你結婚`);
})
yourMsg.listen('unemployment',function (name) {
console.log(`${name}想知道你失業`);
})
yourMsg.trigger('marrgie','張三');
yourMsg.trigger('unemployment','李四');
複製程式碼
- 有時間我們需要取消訂閱的事件,比如李四是你的好朋友,但是因為一件事情,你倆鬧掰了,你把他從你的通訊錄中給刪除掉了,這裡我們給event增加一個remove方法;
remove:function (key,fn) {
var fns = this.clientList[key];
if(!fns){
return false;
}
if(!fn){
fns && (fns.length=0)
}else{
for (let index = 0; index < fns.length; index++) {
const _fn = fns[index];
if(_fn === fn){
fns.splice(index,1);
}
}
}
}
複製程式碼
釋出-訂閱的順序探討
我們通常所看到的都是先訂閱再發布,但是必須要遵守這種順序嗎?答案是不一定的。如果釋出者先發布一條訊息,但是此時還沒有訂閱者訂閱此訊息,我們可以不讓此訊息消失於宇宙之中。就如同QQ離線訊息一樣,離線的訊息被儲存在伺服器中,接收人下次登入之後,才會收到此訊息。同樣的,我們可以建立一個存放離線事件的堆疊,當事件釋出的時候,如果此時還沒有訂閱者訂閱這個事件,我們暫時把釋出事件的動作包裹在一個函式裡,這些包裝函式會被存入堆疊中,等到有物件來訂閱事件的時候,我們將遍歷堆疊並依次執行這些包裝函式,即重發裡面的事件,不過離線事件的生命週期只有一次,就像qq未讀訊息只會提示你一次一樣。
JavaScript實現釋出-訂閱模式的便利性
因為JavaScript有回撥函式這個優勢存在,我們寫開發-訂閱顯得更簡單一點。傳統的釋出-訂閱比如Java通常會把訂閱者自身當成引用傳入釋出者物件中,同時訂閱者物件還需提供一個名為諸如update的方法,供釋出者物件在合適的時候呼叫。下面程式碼用js模擬下傳統的實現。
function Dep() {
this.subs = [];
}
Dep.prototype.addSub = function (sub) {
this.subs.push(sub);
}
Dep.prototype.notify = function () {
this.subs.forEach(sub=>sub.update());
}
function Watcher(fn) {
this.fn = fn;
}
Watcher.prototype.update = function () {
this.fn();
}
var dep = new Dep();
dep.addSub(new Watcher(function () {
console.log('okokok');
}))
dep.notify();
複製程式碼
小結
- 釋出-訂閱的優勢很明顯,做到了時間上的解耦和物件之間的解耦,從架構上看,MVC,MVVM都少不了釋出-訂閱的參與,我們常用的Vue也是基於釋出-訂閱的,最近會抽時間寫下vue的原始碼實現,同樣的node中的EventEmitter也是釋出訂閱的,之前也手寫過它的實現。
- 釋出-訂閱同時也是有缺點存在的,建立訂閱者本身要消耗一定的時間和記憶體,而且當你訂閱一個訊息以後,可能此訊息最後都未發生,但是這個訂閱者會始終存在於記憶體中。如果程式中大量使用釋出-訂閱的話,也會使得程式跟蹤bug變得困難。