javascript
JavaScript 设计模式之观察者模式与发布订阅模式
前言
在軟體工程中,設(shè)計(jì)模式(design pattern)是對軟體設(shè)計(jì)中普遍存在(反復(fù)出現(xiàn))的各種問題,所提出的解決方案。
設(shè)計(jì)模式并不直接用來完成程式碼的編寫,而是描述在各種不同情況下,要怎么解決問題的一種方案。
設(shè)計(jì)模式能使不穩(wěn)定轉(zhuǎn)為相對穩(wěn)定、具體轉(zhuǎn)為相對抽象,避免會(huì)引起麻煩的緊耦合,以增強(qiáng)軟體設(shè)計(jì)面對并適應(yīng)變化的能力
——維基百科
設(shè)計(jì)模式是一種軟件開發(fā)的思想,有益于降低代碼的耦合性,增強(qiáng)代碼的健壯性。往往在大型項(xiàng)目中用的比較多。
今天就來介紹一下觀察者模式與發(fā)布訂閱模式。這在解耦中非常實(shí)用。
什么是觀察者模式?
先舉一個(gè)簡單的例子:
畢業(yè)前,很多同學(xué)都會(huì)說類似于這樣的話:
“老王,等你結(jié)婚了,記得叫我來喝喜酒!”
于是有一天你真的要結(jié)婚了,且需要舉辦酒席,這時(shí)候你需要通知你的你的那些老友來喝喜酒。于是你拿起了手機(jī)給你的那些分布于世界各地的好朋友打起了電話,說了結(jié)婚酒席一事。
到了酒席那天,有的朋友來了,有的人沒來禮卻到了,有的呢只有簡短的兩句祝福,剩下的只有推脫。
這就是觀察者模式
在觀察者模式中,目標(biāo)與觀察者相互獨(dú)立,又相互聯(lián)系:
- 兩者都是相互獨(dú)立的對對象個(gè)體。
- 觀察者在目標(biāo)對象中訂閱事件,目標(biāo)廣播發(fā)布事件。
就像之前的例子一樣:
- 老王就是模式中所謂的目標(biāo)。
- 同學(xué)們在畢業(yè)前說的話就相當(dāng)于在目標(biāo)對象上訂閱事件。
- 老王打電話通知朋友就是發(fā)布事件。
- 同學(xué)們各自作出了不同的行動(dòng)回應(yīng)。
這么說我們的代碼就慢慢建立起來了。
首先我們需要定義兩個(gè)對象:
并且在目標(biāo)對象中要存放觀察者對象的引用,就像老王要存放同學(xué)的手機(jī)好一樣,只有存了才能聯(lián)系嘛。于是我們有了下面的代碼:
function Subject() {this.observers = new ObserverList(); } function ObserverList() {this.observerList = []; } function Observer() {} 復(fù)制代碼對于目標(biāo)對象中的引用,我們必須可以動(dòng)態(tài)的控制:
ObserverList.prototype.add = function(obj) {return this.observerList.push(obj); };ObserverList.prototype.count = function() {return this.observerList.length; };ObserverList.prototype.get = function(index) {if (index > -1 && index < 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); };Subject.prototype.addObserver = function(observer) {this.observers.add(observer); };Subject.prototype.removeObserver = function(observer) {this.observers.removeAt(this.observers.indexOf(observer, 0)); }; 復(fù)制代碼這樣我們就能對老王手機(jī)聯(lián)系人進(jìn)行增、刪、查的操作了。
現(xiàn)在我們就要考慮發(fā)布消息的功能函數(shù)了。首先必須明確一點(diǎn):目標(biāo)對象并不能指定觀察者對象做出什么相應(yīng)的變化。目標(biāo)對象只有通知的作用。就像老王只能告訴朋友他要辦喜酒了,至于朋友接下來怎么辦,則全是朋友自己決定的。
所以我們得寫一個(gè)目標(biāo)廣播消息的功能函數(shù):
Subject.prototype.notify = function(context) {var observerCount = this.observers.count();for (var i = 0; i < observerCount; i++) {this.observers.get(i).update(context);} }; 復(fù)制代碼我們將具體的觀察者對象該作出的變化交給了觀察者對象自己去處理。這就要求觀察者對象需要擁有自己的 update(context)方法來作出改變,同時(shí)該方法不應(yīng)該寫在原型鏈上,因?yàn)槊恳粋€(gè)實(shí)例化后的 Observer 對象所做的響應(yīng)都是不同的,需要獨(dú)立存儲(chǔ) update(context)方法:
function Observer() {this.update = function() {// ...}; } 復(fù)制代碼到此我們就完成了一個(gè)簡單的觀察者模式的構(gòu)建。
完整代碼:
function ObserverList() {this.observerList = []; }ObserverList.prototype.add = function(obj) {return this.observerList.push(obj); };ObserverList.prototype.count = function() {return this.observerList.length; };ObserverList.prototype.get = function(index) {if (index > -1 && index < 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);} };// The Observer function Observer() {this.update = function() {// ...}; } 復(fù)制代碼什么是發(fā)布訂閱模式?
先舉個(gè)簡單的例子:
我們生活中,特別是在一線城市打拼的年輕人,與租房的聯(lián)系再密切不過了。同時(shí)我們的身邊也有很多租房中介。
某天路人甲需要租一套三室一廳一廚一衛(wèi)的房,他找到了中介問了問有沒有。中介看了看發(fā)現(xiàn)并沒有他要的房型,于是和路人甲說:“等有房東提供了此類房型的時(shí)候再聯(lián)系你。”于是你就回去等消息了。
有一天,某一位房東將自己多余的房屋信息以及圖片整理好發(fā)給中介,中介看了看,這不就是路人甲要的房型嗎。于是立馬打電話讓路人甲看房。最終撮合了一單生意。
這就是發(fā)布訂閱模式
可以看出,在發(fā)布訂閱模式中最重要的是 Topic/Event Channel (Event)對象。我們可以簡單的稱之為“中介”。
在這個(gè)中介對象中既要接受發(fā)布者所發(fā)布的消息,又要將消息派發(fā)給訂閱者。所以中介還應(yīng)該按照不同的事件儲(chǔ)存相應(yīng)的訂閱者信息。
首先我們先會(huì)給中介對象的每個(gè)訂閱者對象一個(gè)標(biāo)識(shí),每當(dāng)有一個(gè)新的訂閱者訂閱事件的時(shí)候,我們就給一個(gè) subUid。
我們先來寫一下中介對象(pubsub):
var pubsub = {}; (function(myObject) {var topics = {};var subUid = -1;myObject.publish = function() {};myObject.subscribe = function() {};myObject.unsubscribe = function() {}; })(pubsub); 復(fù)制代碼這里我們用了工廠模式來創(chuàng)建我們的中介對象。
我們先把訂閱功能實(shí)現(xiàn):
首先我們必須認(rèn)識(shí)到 topics 對象將存放著如下類型的數(shù)據(jù):
topics = {topicA: [{token: subuid,function: func},...],topicB: [{token: subuid,function: func},...],... } 復(fù)制代碼對于 topics 對象,存放在許多不同的事件名稱(topicA...),對于每一個(gè)事件都有指定的一個(gè)數(shù)組對象用以存放訂閱該事件的訂閱對象及發(fā)生事件之后作出的響應(yīng)。
所以當(dāng)有訂閱對象在中介中訂閱事件時(shí):
myObject.subscribe = function(topic, func) {//如果不存在相應(yīng)事件就創(chuàng)建一個(gè)if (!topics[topic]) {topics[topic] = [];}//將訂閱對象信息記錄下來var token = (++subUid).toString();topics[topic].push({token: token,func: func});//返回訂閱者標(biāo)識(shí),方標(biāo)在取消訂閱的時(shí)候使用return token; }; 復(fù)制代碼接下來我們來實(shí)現(xiàn)取消訂閱的功能:
我們只需要遍歷 topics 各個(gè)事件中的對象即可。
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; }; 復(fù)制代碼剩下的就是發(fā)布事件的實(shí)現(xiàn)了:
我們只需要給定事件名稱 topic 和相應(yīng)的參數(shù)即可,找到相應(yīng)事件所對應(yīng)的訂閱者列表,遍歷調(diào)用列表中的方法。
myObject.publish = function(topic, args) {if (!topics[topic]) {return false;}var subscribers = topics[topic],len = subscribers ? subscribers.length : 0;while (len--) {subscribers[len].func(args);}return this; }; 復(fù)制代碼至此,我們的中介對象就完成了。在發(fā)布訂閱模式中我們不必在意發(fā)布者和訂閱者。
完整代碼:
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(args);}return this;};myObject.subscribe = function(topic, func) {if (!topics[topic]) {topics[topic] = [];}var token = (++subUid).toString();topics[topic].push({token: token,func: func});return token;};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); 復(fù)制代碼二者的區(qū)別和聯(lián)系
區(qū)別:
聯(lián)系:
實(shí)戰(zhàn)
這里需要一點(diǎn)模板引擎的知識(shí),關(guān)于模板引擎可以看我之前發(fā)的一篇文章:《手?jǐn)] JavaScript 模板引擎》
假如我們有如下模板需要渲染:
var template = `<span><% this.value %></span>`; 復(fù)制代碼該模板依賴的數(shù)據(jù)源如下:
var data = {value: 0 }; 復(fù)制代碼現(xiàn)假若 data 中的 value 時(shí)動(dòng)態(tài)的,每隔一秒加 1。
setInterval(function() {data.value++; }, 1000); 復(fù)制代碼同時(shí)我們也要在頁面上發(fā)生變化,這時(shí)你可能寫出如下代碼:
setInterval(function() {data.value++;document.body.innerHTML = TemplateEngine(template, data); }, 1000); 復(fù)制代碼我們可以對比一下發(fā)布訂閱模式的實(shí)現(xiàn):
var template = `<span><% this.value %></span>`; var data = {value: 0 }; function render() {document.body.innerHTML = TemplateEngine(template, data); } window.onload = function() {render();pubsub.subscribe("change", render);setInterval(function() {data.value++;pubsub.publish("change");}, 1000); }; 復(fù)制代碼前者似乎看起來很簡單明了,但是:
相比之下,發(fā)布訂閱模式就顯得邏輯清晰,已于維護(hù),值得細(xì)細(xì)體味。
值得一提:事件監(jiān)聽的實(shí)現(xiàn)
事件監(jiān)聽是我們經(jīng)常用到的功能,其實(shí)它的實(shí)現(xiàn)就是源自于發(fā)布訂閱模式,不信你看:
subject.addEventListener("click", () => {//... }); 復(fù)制代碼這就是在訂閱一個(gè)事件的調(diào)用。
其實(shí)觀察者模式與發(fā)布訂閱模式與我們息息相關(guān)!?
-EFO-
筆者專門在 github 上創(chuàng)建了一個(gè)倉庫,用于記錄平時(shí)學(xué)習(xí)全棧開發(fā)中的技巧、難點(diǎn)、易錯(cuò)點(diǎn),歡迎大家點(diǎn)擊下方鏈接瀏覽。如果覺得還不錯(cuò),就請給個(gè)小星星吧!?
2019/04/28
AJie
轉(zhuǎn)載于:https://juejin.im/post/5cc57704e51d456e5a072975
總結(jié)
以上是生活随笔為你收集整理的JavaScript 设计模式之观察者模式与发布订阅模式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux环境:NFS--网络文件系统部
- 下一篇: 原生JS实现图片滚动