發佈-訂閱模式,看似陌生,其實否則。工做中常常會用到,例如 Node.js EventEmitter 中的 on 和 emit 方法;Vue 中的$on
和$emit
方法。他們都使用了發佈-訂閱模式,讓開發變得更加高效方便。
發佈-訂閱模式實際上是一種對象間一對多的依賴關係,當一個對象的狀態發送改變時,全部依賴於它的對象都將獲得狀態改變的通知。編程
訂閱者(Subscriber)把本身想訂閱的事件註冊(Subscribe)到調度中心(Event Channel),當發佈者(Publisher)發佈該事件(Publish Event)到調度中心,也就是該事件觸發時,由調度中心統一調度(Fire Event)訂閱者註冊到調度中心的處理代碼。數組
好比咱們很喜歡看某個公衆號號的文章,可是咱們不知道何時發佈新文章,要不定時的去翻閱;這時候,咱們能夠關注該公衆號,當有文章推送時,會有消息及時通知咱們文章更新了。緩存
上面一個看似簡單的操做,實際上是一個典型的發佈訂閱模式,公衆號屬於發佈者,用戶屬於訂閱者;用戶將訂閱公衆號的事件註冊到調度中心,公衆號做爲發佈者,當有新文章發佈時,公衆號發佈該事件到調度中心,調度中心會及時發消息告知用戶。app
咱們來看個簡單的 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 發佈-訂閱模式 */
這一版中咱們補充了一下 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 中怎麼實現 $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 中實現的方法支持訂閱數組事件。this
不少地方都說發佈-訂閱模式是觀察者模式的別名,可是他們真的同樣嗎?是不同的。spa
直接上圖:
prototype
觀察者模式
:觀察者(Observer)直接訂閱(Subscribe)主題(Subject),而當主題被激活的時候,會觸發(Fire Event)觀察者裏的事件。
發佈訂閱模式
:訂閱者(Subscriber)把本身想訂閱的事件註冊(Subscribe)到調度中心(Event Channel),當發佈者(Publisher)發佈該事件(Publish Event)到調度中心,也就是該事件觸發時,由調度中心統一調度(Fire Event)訂閱者註冊到調度中心的處理代碼。
差別
: