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