摘要:發布訂閱模式訂閱者把自己想訂閱的事件注冊到調度中心,當發布者發布該事件到調度中心,也就是該事件觸發時,由調度中心統一調度訂閱者注冊到調度中心的處理代碼。
發布-訂閱模式,看似陌生,其實不然。工作中經常會用到,例如 Node.js EventEmitter 中的 on 和 emit 方法;Vue 中的 $on 和 $emit 方法。他們都使用了發布-訂閱模式,讓開發變得更加高效方便。一、 什么是發布-訂閱模式 1. 定義
發布-訂閱模式其實是一種對象間一對多的依賴關系,當一個對象的狀態發送改變時,所有依賴于它的對象都將得到狀態改變的通知。
訂閱者(Subscriber)把自己想訂閱的事件注冊(Subscribe)到調度中心(Event Channel),當發布者(Publisher)發布該事件(Publish Event)到調度中心,也就是該事件觸發時,由調度中心統一調度(Fire Event)訂閱者注冊到調度中心的處理代碼。
2. 例子比如我們很喜歡看某個公眾號號的文章,但是我們不知道什么時候發布新文章,要不定時的去翻閱;這時候,我們可以關注該公眾號,當有文章推送時,會有消息及時通知我們文章更新了。
上面一個看似簡單的操作,其實是一個典型的發布訂閱模式,公眾號屬于發布者,用戶屬于訂閱者;用戶將訂閱公眾號的事件注冊到調度中心,公眾號作為發布者,當有新文章發布時,公眾號發布該事件到調度中心,調度中心會及時發消息告知用戶。
二、 如何實現發布-訂閱模式? 1. 實現思路創建一個對象
在該對象上創建一個緩存列表(調度中心)
on 方法用來把函數 fn 都加到緩存列表中(訂閱者注冊事件到調度中心)
emit 方法取到 arguments 里第一個當做 event,根據 event 值去執行對應緩存列表中的函數(發布者發布事件到調度中心,調度中心處理代碼)
off 方法可以根據 event 值取消訂閱(取消訂閱)
once 方法只監聽一次,調用完畢后刪除緩存函數(訂閱一次)
2. demo1我們來看個簡單的 demo,實現了 on 和 emit 方法,代碼中有詳細注釋。
// 公眾號對象 let eventEmitter = {}; // 緩存列表,存放 event 及 fn eventEmitter.list = {}; // 訂閱 eventEmitter.on = function (event, fn) { let _this = this; // 如果對象中沒有對應的 event 值,也就是說明沒有訂閱過,就給 event 創建個緩存列表 // 如有對象中有相應的 event 值,把 fn 添加到對應 event 的緩存列表里 (_this.list[event] || (_this.list[event] = [])).push(fn); return _this; }; // 發布 eventEmitter.emit = function () { let _this = this; // 第一個參數是對應的 event 值,直接用數組的 shift 方法取出 let event = [].shift.call(arguments), fns = _this.list[event]; // 如果緩存列表里沒有 fn 就返回 false if (!fns || fns.length === 0) { return false; } // 遍歷 event 值對應的緩存列表,依次執行 fn fns.forEach(fn => { fn.apply(_this, arguments); }); return _this; }; function user1 (content) { console.log("用戶1訂閱了:", content); }; function user2 (content) { console.log("用戶2訂閱了:", content); }; // 訂閱 eventEmitter.on("article", user1); eventEmitter.on("article", user2); // 發布 eventEmitter.emit("article", "Javascript 發布-訂閱模式"); /* 用戶1訂閱了: Javascript 發布-訂閱模式 用戶2訂閱了: Javascript 發布-訂閱模式 */3. demo2
這一版中我們補充了一下 once 和 off 方法。
let eventEmitter = { // 緩存列表 list: {}, // 訂閱 on (event, fn) { let _this = this; // 如果對象中沒有對應的 event 值,也就是說明沒有訂閱過,就給 event 創建個緩存列表 // 如有對象中有相應的 event 值,把 fn 添加到對應 event 的緩存列表里 (_this.list[event] || (_this.list[event] = [])).push(fn); return _this; }, // 監聽一次 once (event, fn) { // 先綁定,調用后刪除 let _this = this; function on () { _this.off(event, on); fn.apply(_this, arguments); } on.fn = fn; _this.on(event, on); return _this; }, // 取消訂閱 off (event, fn) { let _this = this; let fns = _this.list[event]; // 如果緩存列表中沒有相應的 fn,返回false if (!fns) return false; if (!fn) { // 如果沒有傳 fn 的話,就會將 event 值對應緩存列表中的 fn 都清空 fns && (fns.length = 0); } else { // 若有 fn,遍歷緩存列表,看看傳入的 fn 與哪個函數相同,如果相同就直接從緩存列表中刪掉即可 let cb; for (let i = 0, cbLen = fns.length; i < cbLen; i++) { cb = fns[i]; if (cb === fn || cb.fn === fn) { fns.splice(i, 1); break } } } return _this; }, // 發布 emit () { let _this = this; // 第一個參數是對應的 event 值,直接用數組的 shift 方法取出 let event = [].shift.call(arguments), fns = _this.list[event]; // 如果緩存列表里沒有 fn 就返回 false if (!fns || fns.length === 0) { return false; } // 遍歷 event 值對應的緩存列表,依次執行 fn fns.forEach(fn => { fn.apply(_this, arguments); }); return _this; } }; function user1 (content) { console.log("用戶1訂閱了:", content); } function user2 (content) { console.log("用戶2訂閱了:", content); } function user3 (content) { console.log("用戶3訂閱了:", content); } function user4 (content) { console.log("用戶4訂閱了:", content); } // 訂閱 eventEmitter.on("article1", user1); eventEmitter.on("article1", user2); eventEmitter.on("article1", user3); // 取消user2方法的訂閱 eventEmitter.off("article1", user2); eventEmitter.once("article2", user4) // 發布 eventEmitter.emit("article1", "Javascript 發布-訂閱模式"); eventEmitter.emit("article1", "Javascript 發布-訂閱模式"); eventEmitter.emit("article2", "Javascript 觀察者模式"); eventEmitter.emit("article2", "Javascript 觀察者模式"); // eventEmitter.on("article1", user3).emit("article1", "test111"); /* 用戶1訂閱了: Javascript 發布-訂閱模式 用戶3訂閱了: Javascript 發布-訂閱模式 用戶1訂閱了: Javascript 發布-訂閱模式 用戶3訂閱了: Javascript 發布-訂閱模式 用戶4訂閱了: Javascript 觀察者模式 */三、 Vue 中的實現
有了發布-訂閱模式的知識后,我們來看下 Vue 中怎么實現 $on 和 $emit 的方法,直接看源碼:
function eventsMixin (Vue) { var hookRE = /^hook:/; Vue.prototype.$on = function (event, fn) { var this$1 = this; var vm = this; // event 為數組時,循環執行 $on if (Array.isArray(event)) { for (var i = 0, l = event.length; i < l; i++) { this$1.$on(event[i], fn); } } else { (vm._events[event] || (vm._events[event] = [])).push(fn); // optimize hook:event cost by using a boolean flag marked at registration // instead of a hash lookup if (hookRE.test(event)) { vm._hasHookEvent = true; } } return vm }; Vue.prototype.$once = function (event, fn) { var vm = this; // 先綁定,后刪除 function on () { vm.$off(event, on); fn.apply(vm, arguments); } on.fn = fn; vm.$on(event, on); return vm }; Vue.prototype.$off = function (event, fn) { var this$1 = this; var vm = this; // all,若沒有傳參數,清空所有訂閱 if (!arguments.length) { vm._events = Object.create(null); return vm } // array of events,events 為數組時,循環執行 $off if (Array.isArray(event)) { for (var i = 0, l = event.length; i < l; i++) { this$1.$off(event[i], fn); } return vm } // specific event var cbs = vm._events[event]; if (!cbs) { // 沒有 cbs 直接 return this return vm } if (!fn) { // 若沒有 handler,清空 event 對應的緩存列表 vm._events[event] = null; return vm } if (fn) { // specific handler,刪除相應的 handler var cb; var i$1 = cbs.length; while (i$1--) { cb = cbs[i$1]; if (cb === fn || cb.fn === fn) { cbs.splice(i$1, 1); break } } } return vm }; Vue.prototype.$emit = function (event) { var vm = this; { // 傳入的 event 區分大小寫,若不一致,有提示 var lowerCaseEvent = event.toLowerCase(); if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { tip( "Event "" + lowerCaseEvent + "" is emitted in component " + (formatComponentName(vm)) + " but the handler is registered for "" + event + "". " + "Note that HTML attributes are case-insensitive and you cannot use " + "v-on to listen to camelCase events when using in-DOM templates. " + "You should probably use "" + (hyphenate(event)) + "" instead of "" + event + ""." ); } } var cbs = vm._events[event]; if (cbs) { cbs = cbs.length > 1 ? toArray(cbs) : cbs; // 只取回調函數,不取 event var args = toArray(arguments, 1); for (var i = 0, l = cbs.length; i < l; i++) { try { cbs[i].apply(vm, args); } catch (e) { handleError(e, vm, ("event handler for "" + event + """)); } } } return vm }; } /*** * Convert an Array-like object to a real Array. */ function toArray (list, start) { start = start || 0; var i = list.length - start; var ret = new Array(i); while (i--) { ret[i] = list[i + start]; } return ret }
實現思路大體相同,如上第二點中的第一條:實現思路。Vue 中實現的方法支持訂閱數組事件。
四、 總結 1. 優點對象之間解耦
異步編程中,可以更松耦合的代碼編寫
2. 缺點創建訂閱者本身要消耗一定的時間和內存
雖然可以弱化對象之間的聯系,多個發布者和訂閱者嵌套一起的時候,程序難以跟蹤維護
五、 擴展(發布-訂閱模式與觀察者模式的區別)很多地方都說發布-訂閱模式是觀察者模式的別名,但是他們真的一樣嗎?是不一樣的。
直接上圖:
觀察者模式:觀察者(Observer)直接訂閱(Subscribe)主題(Subject),而當主題被激活的時候,會觸發(Fire Event)觀察者里的事件。
發布訂閱模式:訂閱者(Subscriber)把自己想訂閱的事件注冊(Subscribe)到調度中心(Event Channel),當發布者(Publisher)發布該事件(Publish Event)到調度中心,也就是該事件觸發時,由調度中心統一調度(Fire Event)訂閱者注冊到調度中心的處理代碼。
差異:
在觀察者模式中,觀察者是知道 Subject 的,Subject 一直保持對觀察者進行記錄。然而,在發布訂閱模式中,發布者和訂閱者不知道對方的存在。它們只有通過消息代理進行通信。
在發布訂閱模式中,組件是松散耦合的,正好和觀察者模式相反。
觀察者模式大多數時候是同步的,比如當事件觸發,Subject 就會去調用觀察者的方法。而發布-訂閱模式大多數時候是異步的(使用消息隊列)。
觀察者模式需要在單個應用程序地址空間中實現,而發布-訂閱更像交叉應用模式。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/109747.html
摘要:設計模式與開發實踐讀書筆記。發布訂閱模式又叫觀察者模式,它定義了對象之間的一種一對多的依賴關系。附設計模式之發布訂閱模式觀察者模式數據結構和算法系列棧隊列優先隊列循環隊列設計模式系列設計模式之策略模式 《JavaScript設計模式與開發實踐》讀書筆記。 發布-訂閱模式又叫觀察者模式,它定義了對象之間的一種一對多的依賴關系。當一個對象的狀態發生改變時,所有依賴它的對象都將得到通知。 例...
摘要:設計模式與開發實踐讀書筆記。看此文章前,建議先看設計模式之發布訂閱模式觀察者模式在中,已經介紹了什么是發布訂閱模式,同時,也實現了發布訂閱模式。 《JavaScript設計模式與開發實踐》讀書筆記。 看此文章前,建議先看JavaScript設計模式之發布-訂閱模式(觀察者模式)-Part1 在Part1中,已經介紹了什么是發布-訂閱模式,同時,也實現了發布-訂閱模式。但是,就Part1...
摘要:設計模式與開發實踐讀書筆記。看此文章前,建議先看設計模式之發布訂閱模式觀察者模式在中,已經介紹了什么是發布訂閱模式,同時,也實現了發布訂閱模式。 《JavaScript設計模式與開發實踐》讀書筆記。 看此文章前,建議先看JavaScript設計模式之發布-訂閱模式(觀察者模式)-Part1 在Part1中,已經介紹了什么是發布-訂閱模式,同時,也實現了發布-訂閱模式。但是,就Part1...
閱讀 3339·2022-01-04 14:20
閱讀 3107·2021-09-22 15:08
閱讀 2188·2021-09-03 10:44
閱讀 2316·2019-08-30 15:44
閱讀 1491·2019-08-29 18:40
閱讀 2655·2019-08-29 17:09
閱讀 2989·2019-08-26 13:53
閱讀 3221·2019-08-26 13:37