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