一、概念
1.定义
发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到状态改变的通知。
订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
二、实现
1.实现思路
- 创建一个对象
- 在该对象上创建一个缓存列表(调度中心Event Channel)
- on方法用来把函数添加到缓存列表中(订阅者注册事件到调度中心)
- emit方法取到argument里第一个当作event,根据event值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)
- off方法可以根据event值取消订阅(取消订阅)
- once方法只监听一次,调用完毕后删除缓存函数(订阅一次)
2.简单demo
class EventEmitter {
constructor() {
// 缓存列表
this.listener = {};
}
// 订阅
on(eventName, fn) {
// 如果对象中没有对应的 event 值,也就是说明没有订阅过,就给 event 创建个缓存列表
// 如有对象中有相应的 event 值,把 fn 添加到对应 event 的缓存列表里
if (!this.listener[eventName]) {
this.listener[eventName] = [];
}
this.listener[eventName].push(fn);
}
// 取消订阅
off(eventName, fn) {
let callbacks = this.listener[eventName];
// 缓存列表中没有对应的fn,返回false
if (!callbacks) {
return false;
}
if (!fn) {
// 如果未传入fn,则将缓存列表中对应的fn都清空
callbacks && (callbacks.length = 0);
} else {
let cb;
// 遍历所对应的fn,判断和那个fn相同,相同则删除
for (let i = 0, cbLen = callbacks.length; i < cbLen; i++) {
cb = callbacks[i];
if (cb == fn || cb.fn == fn) {
callbacks.splice(i, 1);
break;
}
}
}
}
// 监听一次
once(eventName, fn) {
// 先绑定,运行时删除对应的值
let on = () => {
this.off(eventName, on);
fn.apply(this, arguments);
};
on.fn = fn;
this.on(eventName, on);
}
// 发布
emit(eventName, data) {
const callbacks = this.listener[eventName];
if (callbacks) {
callbacks.forEach((c) => {
c(data);
});
}
}
}
let a = new EventEmitter();
function aa(x) {
console.log(x);
}
a.on("kak", aa);
a.on("kak", (data) => {
console.log("1", data);
});
a.emit("kak", "hahahah");
a.off("kak", aa);
a.emit("kak", "hahahah");
3.vue中的event Bus
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;
}
三、 总结
1、优点
- 对象之间解耦
- 异步编程中,可以更松耦合的代码编写
2、缺点
- 创建订阅者本身要消耗一定的时间和内存
- 虽然可以弱化对象之间的联系,多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护