javascript設計模式實現之-釋出-訂閱模式(又稱觀察者模式)
相對而言,設計模式概念在前端開發中其實沒有那麼受重視,很多小夥伴可能都做過好幾年的前端開發也並沒有用到設計模式相關知識,然後沒有用到並不等於以後也不會用到,更不等於真的就不重要。
還是先來看下設計模式(Design pattern)的定義:設計模式是軟體開發人員在軟體開發過程中面臨的一般問題的解決方案。這些解決方案是眾多軟體開發人員經過相當長的一段時間的試驗和錯誤總結出來的(摘自 菜鳥教程 https://www.runoob.com/design-pattern/design-pattern-tutorial.html) 。既然是一般問題的解決方案,必然也會適用於前端,而實際上前端知識中最基礎的dom事件機制就用到了 設計模式中的 釋出訂閱模式,Promise的實現也用到了釋出訂閱模式,最近幾年國內流行的SPA(單頁面應用)開發框架 vue同樣用到了釋出訂閱模式。本文就嘗試對 在前端開發中 應用相對較多的 釋出-訂閱模式作下解析。
以下斜體部份摘自《javascript設計模式與開發實踐》
不論是在程式世界裡還是現實生活中,釋出-訂閱模式的應用都非常之廣泛。我們先看一個現實中的例子。小明最近看上了一套房子,到了售樓處之後才被告知,該樓盤的房子早已售罄。好在售樓MM 告訴小明,不久後還有一些尾盤推出,開發商正在辦理相關手續,手續辦好後便可以購買。但到底是什麼時候,目前還沒有人能夠知道。於是小明記下了售樓處的電話,以後每天都會打電話過去詢問是不是已經到了購買時間。除了小明,還有小紅、小強、小龍也會每天向售樓處諮詢這個問題。一個星期過後,售樓 MM 決定辭職,因為厭倦了每天回答 1000 個相同內容的電話。當然現實中沒有這麼笨的銷售公司,實際上故事是這樣的:小明離開之前,把電話號碼留在
在以上例子中,傳送簡訊通知就是一個典型的釋出-訂閱模式,小明、小紅等購買者都是訂閱者,他們訂閱了房子開售的訊息。售樓處作為釋出者,會在合適的時候遍歷花名冊上的電話號碼,依次給購房者釋出訊息。
可以發現,在這個例子中使用釋出—訂閱模式有著顯而易見的優點。
- 購房者不用再天天給售樓處打電話諮詢開售時間,在合適的時間點,售樓處作為釋出者會通知這些訊息訂閱者。
- 購房者和售樓處之間不再強耦合在一起,當有新的購房者出現時,他只需把手機號碼留
第一點說明發布—訂閱模式可以廣泛應用於非同步程式設計中,這是一種替代傳遞迴調函式的方案。比如,我們可以訂閱 ajax 請求的 error、 succ 等事件。 或者如果想在動畫的每一幀完成之後做一些事情,那我們可以訂閱一個事件,然後在動畫的每一幀完成之後釋出這個事件。在非同步程式設計中使用釋出—訂閱模式,我們就無需過多關注物件在非同步執行期間的內部狀態,而只需要訂閱感興趣的事件發生點。
第二點說明發布—訂閱模式可以取代物件之間硬編碼的通知機制,一個物件不用再顯式地調用另外一個物件的某個介面。釋出—訂閱模式讓兩個物件鬆耦合地聯絡在一起,雖然不太清楚彼此的細節,但這不影響它們之間相互通訊。當有新的訂閱者出現時,釋出者的程式碼不需要任何修改;同樣釋出者需要改變時,也不會影響到之前的訂閱者。只要之前約定的事件名沒有變化,就可以自由地改變它們。
以下為 釋出訂閱模式 的 一種程式碼實現:
class PubSub { constructor() { this.handles = {}; }// 訂閱 on(eventType, handle) { if (!this.handles.hasOwnProperty(eventType)) { this.handles[eventType] = []; } if (typeof handle == 'function') { this.handles[eventType].push(handle); } else { throw new Error('No callback'); } return this; }
// 釋出 emit(eventType, ...args) { if (this.handles.hasOwnProperty(eventType)) { this.handles[eventType].forEach((item, key, arr) => { item.apply(null, args); }); } else { throw new Error(`"${eventType}"事件未註冊`); } return this; }
// 刪除 off(eventType, handle) { if (!this.handles.hasOwnProperty(eventType)) { throw new Error(`"${eventType}"事件未註冊`); } else if (typeof handle != 'function') { throw new Error('No callback'); } else { this.handles[eventType].forEach((item, key, arr) => { if (item == handle) { arr.splice(key, 1); } }); } return this; // 實現鏈式操作 } }
// 以下為後續要訂閱的一個動作 let callback = function () { console.log('you are so nice'); };
let pubsub = new PubSub(); // 訂閱 pubsub .on('completed', (...args) => { console.log(args.join(' ')); }) .on('completed', callback); // 釋出
pubsub.emit('completed', 'I love you', 'baby'); // pubsub.off('completed', callback); pubsub.emit('completed', 'I love you so much!');
輸出如下:
I love you baby
you are so nice
I love you so much!
you are so nice