javascript
JavaScript 发布-订阅模式
發布-訂閱模式,看似陌生,其實不然。工作中經常會用到,例如 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 就返回 falseif (!fns || fns.length === 0) {return false;}// 遍歷 event 值對應的緩存列表,依次執行 fnfns.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,返回falseif (!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 就返回 falseif (!fns || fns.length === 0) {return false;}// 遍歷 event 值對應的緩存列表,依次執行 fnfns.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 為數組時,循環執行 $onif (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 lookupif (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 為數組時,循環執行 $offif (Array.isArray(event)) {for (var i = 0, l = event.length; i < l; i++) {this$1.$off(event[i], fn);}return vm}// specific eventvar cbs = vm._events[event];if (!cbs) {// 沒有 cbs 直接 return thisreturn vm}if (!fn) {// 若沒有 handler,清空 event 對應的緩存列表vm._events[event] = null;return vm}if (fn) {// specific handler,刪除相應的 handlervar 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;// 只取回調函數,不取 eventvar 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 就會去調用觀察者的方法。而發布-訂閱模式大多數時候是異步的(使用消息隊列)。
-
觀察者模式需要在單個應用程序地址空間中實現,而發布-訂閱更像交叉應用模式。
轉載于:https://juejin.im/post/5ce75fe16fb9a07eb67d6999
總結
以上是生活随笔為你收集整理的JavaScript 发布-订阅模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Facebook推出Pythia 开源
- 下一篇: 从零开始学springboot笔记(二)