摘要:發(fā)布訂閱模式訂閱者把自己想訂閱的事件注冊到調(diào)度中心,當(dāng)發(fā)布者發(fā)布該事件到調(diào)度中心,也就是該事件觸發(fā)時,由調(diào)度中心統(tǒng)一調(diào)度訂閱者注冊到調(diào)度中心的處理代碼。
發(fā)布-訂閱模式,看似陌生,其實不然。工作中經(jīng)常會用到,例如 Node.js EventEmitter 中的 on 和 emit 方法;Vue 中的 $on 和 $emit 方法。他們都使用了發(fā)布-訂閱模式,讓開發(fā)變得更加高效方便。一、 什么是發(fā)布-訂閱模式 1. 定義
發(fā)布-訂閱模式其實是一種對象間一對多的依賴關(guān)系,當(dāng)一個對象的狀態(tài)發(fā)送改變時,所有依賴于它的對象都將得到狀態(tài)改變的通知。
訂閱者(Subscriber)把自己想訂閱的事件注冊(Subscribe)到調(diào)度中心(Event Channel),當(dāng)發(fā)布者(Publisher)發(fā)布該事件(Publish Event)到調(diào)度中心,也就是該事件觸發(fā)時,由調(diào)度中心統(tǒng)一調(diào)度(Fire Event)訂閱者注冊到調(diào)度中心的處理代碼。
2. 例子比如我們很喜歡看某個公眾號號的文章,但是我們不知道什么時候發(fā)布新文章,要不定時的去翻閱;這時候,我們可以關(guān)注該公眾號,當(dāng)有文章推送時,會有消息及時通知我們文章更新了。
上面一個看似簡單的操作,其實是一個典型的發(fā)布訂閱模式,公眾號屬于發(fā)布者,用戶屬于訂閱者;用戶將訂閱公眾號的事件注冊到調(diào)度中心,公眾號作為發(fā)布者,當(dāng)有新文章發(fā)布時,公眾號發(fā)布該事件到調(diào)度中心,調(diào)度中心會及時發(fā)消息告知用戶。
二、 如何實現(xiàn)發(fā)布-訂閱模式? 1. 實現(xiàn)思路創(chuàng)建一個對象
在該對象上創(chuàng)建一個緩存列表(調(diào)度中心)
on 方法用來把函數(shù) fn 都加到緩存列表中(訂閱者注冊事件到調(diào)度中心)
emit 方法取到 arguments 里第一個當(dāng)做 event,根據(jù) event 值去執(zhí)行對應(yīng)緩存列表中的函數(shù)(發(fā)布者發(fā)布事件到調(diào)度中心,調(diào)度中心處理代碼)
off 方法可以根據(jù) event 值取消訂閱(取消訂閱)
once 方法只監(jiān)聽一次,調(diào)用完畢后刪除緩存函數(shù)(訂閱一次)
2. demo1我們來看個簡單的 demo,實現(xiàn)了 on 和 emit 方法,代碼中有詳細注釋。
// 公眾號對象 let eventEmitter = {}; // 緩存列表,存放 event 及 fn eventEmitter.list = {}; // 訂閱 eventEmitter.on = function (event, fn) { let _this = this; // 如果對象中沒有對應(yīng)的 event 值,也就是說明沒有訂閱過,就給 event 創(chuàng)建個緩存列表 // 如有對象中有相應(yīng)的 event 值,把 fn 添加到對應(yīng) event 的緩存列表里 (_this.list[event] || (_this.list[event] = [])).push(fn); return _this; }; // 發(fā)布 eventEmitter.emit = function () { let _this = this; // 第一個參數(shù)是對應(yīng)的 event 值,直接用數(shù)組的 shift 方法取出 let event = [].shift.call(arguments), fns = _this.list[event]; // 如果緩存列表里沒有 fn 就返回 false if (!fns || fns.length === 0) { return false; } // 遍歷 event 值對應(yīng)的緩存列表,依次執(zhí)行 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); // 發(fā)布 eventEmitter.emit("article", "Javascript 發(fā)布-訂閱模式"); /* 用戶1訂閱了: Javascript 發(fā)布-訂閱模式 用戶2訂閱了: Javascript 發(fā)布-訂閱模式 */3. demo2
這一版中我們補充了一下 once 和 off 方法。
let eventEmitter = { // 緩存列表 list: {}, // 訂閱 on (event, fn) { let _this = this; // 如果對象中沒有對應(yīng)的 event 值,也就是說明沒有訂閱過,就給 event 創(chuàng)建個緩存列表 // 如有對象中有相應(yīng)的 event 值,把 fn 添加到對應(yīng) event 的緩存列表里 (_this.list[event] || (_this.list[event] = [])).push(fn); return _this; }, // 監(jiān)聽一次 once (event, fn) { // 先綁定,調(diào)用后刪除 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]; // 如果緩存列表中沒有相應(yīng)的 fn,返回false if (!fns) return false; if (!fn) { // 如果沒有傳 fn 的話,就會將 event 值對應(yīng)緩存列表中的 fn 都清空 fns && (fns.length = 0); } else { // 若有 fn,遍歷緩存列表,看看傳入的 fn 與哪個函數(shù)相同,如果相同就直接從緩存列表中刪掉即可 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; }, // 發(fā)布 emit () { let _this = this; // 第一個參數(shù)是對應(yīng)的 event 值,直接用數(shù)組的 shift 方法取出 let event = [].shift.call(arguments), fns = _this.list[event]; // 如果緩存列表里沒有 fn 就返回 false if (!fns || fns.length === 0) { return false; } // 遍歷 event 值對應(yīng)的緩存列表,依次執(zhí)行 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) // 發(fā)布 eventEmitter.emit("article1", "Javascript 發(fā)布-訂閱模式"); eventEmitter.emit("article1", "Javascript 發(fā)布-訂閱模式"); eventEmitter.emit("article2", "Javascript 觀察者模式"); eventEmitter.emit("article2", "Javascript 觀察者模式"); // eventEmitter.on("article1", user3).emit("article1", "test111"); /* 用戶1訂閱了: Javascript 發(fā)布-訂閱模式 用戶3訂閱了: Javascript 發(fā)布-訂閱模式 用戶1訂閱了: Javascript 發(fā)布-訂閱模式 用戶3訂閱了: Javascript 發(fā)布-訂閱模式 用戶4訂閱了: Javascript 觀察者模式 */三、 Vue 中的實現(xiàn)
有了發(fā)布-訂閱模式的知識后,我們來看下 Vue 中怎么實現(xiàn) $on 和 $emit 的方法,直接看源碼:
function eventsMixin (Vue) { var hookRE = /^hook:/; Vue.prototype.$on = function (event, fn) { var this$1 = this; var vm = this; // event 為數(shù)組時,循環(huán)執(zhí)行 $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,若沒有傳參數(shù),清空所有訂閱 if (!arguments.length) { vm._events = Object.create(null); return vm } // array of events,events 為數(shù)組時,循環(huán)執(zhí)行 $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 對應(yīng)的緩存列表 vm._events[event] = null; return vm } if (fn) { // specific handler,刪除相應(yīng)的 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 區(qū)分大小寫,若不一致,有提示 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; // 只取回調(diào)函數(shù),不取 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 }
實現(xiàn)思路大體相同,如上第二點中的第一條:實現(xiàn)思路。Vue 中實現(xiàn)的方法支持訂閱數(shù)組事件。
四、 總結(jié) 1. 優(yōu)點對象之間解耦
異步編程中,可以更松耦合的代碼編寫
2. 缺點創(chuàng)建訂閱者本身要消耗一定的時間和內(nèi)存
雖然可以弱化對象之間的聯(lián)系,多個發(fā)布者和訂閱者嵌套一起的時候,程序難以跟蹤維護
五、 擴展(發(fā)布-訂閱模式與觀察者模式的區(qū)別)很多地方都說發(fā)布-訂閱模式是觀察者模式的別名,但是他們真的一樣嗎?是不一樣的。
直接上圖:
觀察者模式:觀察者(Observer)直接訂閱(Subscribe)主題(Subject),而當(dāng)主題被激活的時候,會觸發(fā)(Fire Event)觀察者里的事件。
發(fā)布訂閱模式:訂閱者(Subscriber)把自己想訂閱的事件注冊(Subscribe)到調(diào)度中心(Event Channel),當(dāng)發(fā)布者(Publisher)發(fā)布該事件(Publish Event)到調(diào)度中心,也就是該事件觸發(fā)時,由調(diào)度中心統(tǒng)一調(diào)度(Fire Event)訂閱者注冊到調(diào)度中心的處理代碼。
差異:
在觀察者模式中,觀察者是知道 Subject 的,Subject 一直保持對觀察者進行記錄。然而,在發(fā)布訂閱模式中,發(fā)布者和訂閱者不知道對方的存在。它們只有通過消息代理進行通信。
在發(fā)布訂閱模式中,組件是松散耦合的,正好和觀察者模式相反。
觀察者模式大多數(shù)時候是同步的,比如當(dāng)事件觸發(fā),Subject 就會去調(diào)用觀察者的方法。而發(fā)布-訂閱模式大多數(shù)時候是異步的(使用消息隊列)。
觀察者模式需要在單個應(yīng)用程序地址空間中實現(xiàn),而發(fā)布-訂閱更像交叉應(yīng)用模式。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/109747.html
摘要:設(shè)計模式與開發(fā)實踐讀書筆記。發(fā)布訂閱模式又叫觀察者模式,它定義了對象之間的一種一對多的依賴關(guān)系。附設(shè)計模式之發(fā)布訂閱模式觀察者模式數(shù)據(jù)結(jié)構(gòu)和算法系列棧隊列優(yōu)先隊列循環(huán)隊列設(shè)計模式系列設(shè)計模式之策略模式 《JavaScript設(shè)計模式與開發(fā)實踐》讀書筆記。 發(fā)布-訂閱模式又叫觀察者模式,它定義了對象之間的一種一對多的依賴關(guān)系。當(dāng)一個對象的狀態(tài)發(fā)生改變時,所有依賴它的對象都將得到通知。 例...
摘要:設(shè)計模式與開發(fā)實踐讀書筆記??创宋恼虑?,建議先看設(shè)計模式之發(fā)布訂閱模式觀察者模式在中,已經(jīng)介紹了什么是發(fā)布訂閱模式,同時,也實現(xiàn)了發(fā)布訂閱模式。 《JavaScript設(shè)計模式與開發(fā)實踐》讀書筆記。 看此文章前,建議先看JavaScript設(shè)計模式之發(fā)布-訂閱模式(觀察者模式)-Part1 在Part1中,已經(jīng)介紹了什么是發(fā)布-訂閱模式,同時,也實現(xiàn)了發(fā)布-訂閱模式。但是,就Part1...
摘要:設(shè)計模式與開發(fā)實踐讀書筆記??创宋恼虑?,建議先看設(shè)計模式之發(fā)布訂閱模式觀察者模式在中,已經(jīng)介紹了什么是發(fā)布訂閱模式,同時,也實現(xiàn)了發(fā)布訂閱模式。 《JavaScript設(shè)計模式與開發(fā)實踐》讀書筆記。 看此文章前,建議先看JavaScript設(shè)計模式之發(fā)布-訂閱模式(觀察者模式)-Part1 在Part1中,已經(jīng)介紹了什么是發(fā)布-訂閱模式,同時,也實現(xiàn)了發(fā)布-訂閱模式。但是,就Part1...
閱讀 3404·2022-01-04 14:20
閱讀 3118·2021-09-22 15:08
閱讀 2208·2021-09-03 10:44
閱讀 2324·2019-08-30 15:44
閱讀 1501·2019-08-29 18:40
閱讀 2669·2019-08-29 17:09
閱讀 2996·2019-08-26 13:53
閱讀 3226·2019-08-26 13:37