詳情我的博客:https://shengchangwei.github.io/js-shejimoshi-fabudingyue/javascript
發佈-訂閱模式:發佈—訂閱模式又叫觀察者模式,它定義對象間的一種一對多的依賴關係,當一個對象的狀
態發生改變時,全部依賴於它的對象都將獲得通知。java
發佈—訂閱模式能夠普遍應用於異步編程中, 這是一種替代傳遞迴調函數的方案。無需過多關注對象在異步運行期間的內部狀態,而只須要訂閱感興趣的事件發生點。git
發佈—訂閱模式能夠取代對象之間硬編碼的通知機制,一個對象不用再顯式地調用另一個對象的某個接口。發佈—訂閱模式讓兩個對象鬆耦合地聯繫在一塊兒,雖然不太清楚彼此的細節,但這不影響它們之間相互通訊。github
只要咱們曾經在 DOM 節點上面綁定過事件函數,那咱們就曾經使用過發佈—訂閱模式,來看看下面這兩句簡單的代碼發生了什麼事情:ajax
// 監聽全局點擊事件,(訂閱全局點擊事件) document.body.addEventListener('click', function() { console.log('觸發點擊事件') }, false) document.body.click(); // 模擬用戶點擊 (發佈點擊事件)
需求介紹:
小明最近看上了一套房子,到了售樓處以後才被告知,該樓盤的房子早已售罄。好在售樓MM 告訴小明,不久後還有一些尾盤推出,開發商正在辦理相關手續,手續辦好後即可以購買。但究竟是何時,目前尚未人可以知道。編程
因而小明記下了售樓處的電話,之後天天都會打電話過去詢問是否是已經到了購買時間。除了小明,還有小紅、小強、小龍也會天天向售樓處諮詢這個問題。一個星期事後,售樓 MM 決定辭職,由於厭倦了天天回答 1000 個相同內容的電話。設計模式
固然現實中沒有這麼笨的銷售公司,實際上故事是這樣的:小明離開以前,把電話號碼留在了售樓處。售樓 MM 答應他,新樓盤一推出就立刻發信息通知小明。小紅、小強和小龍也是同樣,他們的電話號碼都被記在售樓處的花名冊上,新樓盤推出的時候,售樓 MM 會翻開花名冊,遍歷上面的電話號碼,依次發送一條短信來通知他們。緩存
實現發佈—訂閱模式思路:服務器
代碼以下:架構
// 定義售樓處 let event = { clientList: [], // 緩存列表,存放訂閱者的回調函數 subscribe: function(fn) { //訂閱事件 this.clientList.push(fn); }, publish: function() { for(var i = 0,fn; fn = this.clientList[i++];) { fn.apply(this, arguments); // arguments 是發佈消息時帶上的參數 } } } // 下面簡單測試 event.subscribe(function(price, squareMeter) { console.log('價格=' + price); console.log('平方米=' + squareMeter); }) event.publish(200000, 88); // 輸出: 200萬,88平方米 event.trigger(3000000, 110); // 輸出:300 萬,110 平方米
至此,咱們已經實現了一個最簡單的發佈—訂閱模式,但這裏還存在一些問題。咱們看到訂閱者接收到了發佈者發佈的每一個消息,雖然小明只想買 88 平方米的房子,可是發佈者把 110 平
方米的信息也推送給了小明,這對小明來講是沒必要要的困擾。因此咱們有必要增長一個標示 key,讓訂閱者只訂閱本身感興趣的消息。改寫後的代碼以下:
let event = { clientList: [], // 緩存列表,存放訂閱者的回調函數 subscribe: function(key, fn) { //訂閱事件 if ( !this.clientList[ key ] ){ // 若是尚未訂閱過此類消息,給該類消息建立一個緩存列表 this.clientList[ key ] = []; } this.clientList[ key ].push( fn ); // 訂閱的消息添加進消息緩存列表 }, publish: function() { var key = Array.prototype.shift.call( arguments ), // 取出消息類型 fns = this.clientList[ key ]; // 取出該消息對應的回調函數集合 if ( !fns || fns.length === 0 ){ // 若是沒有訂閱該消息,則返回 return false; } for( var i = 0, fn; fn = fns[ i++ ]; ){ fn.apply( this, arguments ); // (2) // arguments 是發佈消息時附送的參數 } } } // 測試 // 訂閱事件 event.subscribe('test0', function(data) { console.log('數據0:' + data); }); event.subscribe('test0', function(data) { console.log('數據1:' + data); }); event.subscribe('test1', function(data) { console.log(data); }); setTimeout(function() { count++ // 發佈事件 event.publish('test0', count) event.publish('test1', count) }, 1000)
有時候,咱們也許須要取消訂閱事件的功能。好比小明忽然不想買房子了,爲了不繼續接收到售樓處推送過來的短信,小明須要取消以前訂閱的事件。如今咱們給 event 對象增長unsubscribe方法:
event.unsubscribe = function(key, fn) { const t = this.clientList[key]; if (!t) { // 若是 key 對應的消息沒有被人訂閱,則直接返回 return false; } if (!fn) { // 若是不指定處理方法,則取消該事件下全部的處理方法 delete this.clientList[key]; return true; } // 找到指定取消的處理方法的位置 const i = t.indexOf(fn); if (i < 0) { return false; } t.splice(i, 1); // 若是事件下的處理方法爲空則刪除該事件 if (!t.length) { delete this.clientList[key]; } return true; }
在程序中,發佈—訂閱模式能夠用一個全局的 Event 對象來實現,訂閱者不須要了解消息來自哪一個發佈者,發佈者也不知道消息會推送給哪些訂閱者,Event 做爲一個相似「中介者」的角色,把訂閱者和發佈者聯繫起來。見以下代碼:
var Event = (function(){ var clientList = [], subscribe, publish, unsubscribe; subscribe = function() { //訂閱事件 if ( !this.clientList[ key ] ){ // 若是尚未訂閱過此類消息,給該類消息建立一個緩存列表 this.clientList[ key ] = []; } this.clientList[ key ].push( fn ); // 訂閱的消息添加進消息緩存列表 }; publish = function() { var key = Array.prototype.shift.call( arguments ), // 取出消息類型 fns = this.clientList[ key ]; // 取出該消息對應的回調函數集合 if ( !fns || fns.length === 0 ){ // 若是沒有訂閱該消息,則返回 return false; } for( var i = 0, fn; fn = fns[ i++ ]; ){ fn.apply( this, arguments ); // (2) // arguments 是發佈消息時附送的參數 } }; unsubscribe = function(key, fn) { const t = this.clientList[key]; if (!t) { // 若是 key 對應的消息沒有被人訂閱,則直接返回 return false; } if (!fn) { // 若是不指定處理方法,則取消該事件下全部的處理方法 delete this.clientList[key]; return true; } // 找到指定取消的處理方法的位置 const i = t.indexOf(fn); if (i < 0) { return false; } t.splice(i, 1); // 若是事件下的處理方法爲空則刪除該事件 if (!t.length) { delete this.clientList[key]; } return true; }; return { subscribe: listen, publish: trigger, unsubscribe: remove } })()
第一次接觸發布-訂閱模式是前組長自定義全局Angular事件服務,用來在全應用中經過發佈/訂閱事件來進行通訊,也曾閱讀學習,受益良多,特此致謝!
代碼以下:
源碼連接
import { Injectable } from '@angular/core'; /** * @name EventsService * @description 自定義全局事件服務,用來在全應用中經過發佈/訂閱事件來進行通訊 * @usage * ```ts * import { EventsService } from '../services/events.service'; * * constructor(public events: EventsService) {} * * // 訂閱事件並打印信息 * this.events.subscribe('test', (data: any) => { * console.log(data); // '我是test事件發送來的信息!' * }); * * // 發佈事件 * this.events.publish('test', '我是test事件發送來的信息!'); * * // 取消訂閱 * this.events.unsubscribe('test'); * ``` */ @Injectable() export class EventsService { private channels: any = []; /** * 經過事件主題訂閱相應事件 * @param {string} topic 訂閱事件的主題 * @param {function[]} handlers 事件處理方法 */ subscribe(topic: string, ...handlers: Function[]): void { if (!this.channels[topic]) { this.channels[topic] = []; } handlers.forEach((handler) => { this.channels[topic].push(handler); }); } /** * 經過事件主題取消訂閱相應事件 * @param {string} topic 取消訂閱事件的主題 * @param {function} handler 指定取消該事件主下的處理方法 * @returns {boolean} 取消成功返回true */ unsubscribe(topic: string, handler: Function = null): boolean { const t = this.channels[topic]; if (!t) { return false; } if (!handler) { // 若是不指定處理方法,則取消該事件下全部的處理方法 delete this.channels[topic]; return true; } // 找到指定取消的處理方法的位置 const i = t.indexOf(handler); if (i < 0) { return false; } t.splice(i, 1); // 若是事件下的處理方法爲空則刪除該事件 if (!t.length) { delete this.channels[topic]; } return true; } /** * 經過事件主題發佈相應事件 * @param {string} topic 發佈事件的主題 * @param {any[]} args 經過事件發送的數據 * @returns {any[]} */ publish(topic: string, ...args: any[]): any[] { const t = this.channels[topic]; if (!t) { return null; } const responses: any[] = []; t.forEach((handler: any) => { responses.push(handler(...args)); }); return responses; } }
咱們所瞭解到的發佈—訂閱模式,都是訂閱者必須先訂閱一個消息,隨後才能接收到發佈者發佈的消息。若是把順序反過來,發佈者先發布一條消息,而在此以前並無對象來訂閱它,這條消息無疑將消失在宇宙中。
在某些狀況下,咱們須要先將這條消息保存下來,等到有對象來訂閱它的時候,再從新把消息發佈給訂閱者。就如同 QQ 中的離線消息同樣,離線消息被保存在服務器中,接收人下次登陸上線以後,能夠從新收到這條消息。
這種需求在實際項目中是存在的,好比在以前的商城網站中,獲取到用戶信息以後才能渲染用戶導航模塊,而獲取用戶信息的操做是一個 ajax 異步請求。當 ajax 請求成功返回以後會發佈一個事件,在此以前訂閱了此事件的用戶導航模塊能夠接收到這些用戶信息。
可是這只是理想的情況,由於異步的緣由,咱們不能保證 ajax 請求返回的時間,有時候它返回得比較快,而此時用戶導航模塊的代碼尚未加載好(尚未訂閱相應事件),特別是在用了一些模塊化惰性加載的技術後,這是極可能發生的事情。也許咱們還須要一個方案,使得咱們的發佈—訂閱對象擁有先發布後訂閱的能力。
爲了知足這個需求,咱們要創建一個存放離線事件的堆棧,當事件發佈的時候,若是此時尚未訂閱者來訂閱這個事件,咱們暫時把發佈事件的動做包裹在一個函數裏,這些包裝函數將被存入堆棧中,等到終於有對象來訂閱此事件的時候,咱們將遍歷堆棧而且依次執行這些包裝函數,也就是從新發布里面的事件。固然離線事件的生命週期只有一次,就像 QQ 的未讀消息只會被從新閱讀一次,因此剛纔的操做咱們只能進行一次。
發佈—訂閱模式的優勢很是明顯,一爲時間上的解耦,二爲對象之間的解耦。它的應用很是普遍,既能夠用在異步編程中,也能夠幫助咱們完成更鬆耦合的代碼編寫。發佈—訂閱模式還能夠用來幫助實現一些別的設計模式,好比中介者模式。從架構上來看,不管是 MVC 仍是 MVVM,都少不了發佈—訂閱模式的參與,並且 JavaScript 自己也是一門基於事件驅動的語言。
固然,發佈—訂閱模式也不是徹底沒有缺點。建立訂閱者自己要消耗必定的時間和內存,並且當你訂閱一個消息後,也許此消息最後都未發生,但這個訂閱者會始終存在於內存中。另外,發佈—訂閱模式雖然能夠弱化對象之間的聯繫,但若是度使用的話,對象和對象之間的必要聯繫也將被深埋在背後,會致使程序難以跟蹤維護和理解。特別是有多個發佈者和訂閱者嵌套到一塊兒的時候,要跟蹤一個 bug 不是件輕鬆的事情。