定义
发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。
订阅者把自己想订阅的事件注册到调度中心,当发布者发布该事件到调度中心,也就是该事件触发时,由调度中心统一调度订阅者注册到调度中心的处理代码。
优缺点
优点
- 对象之间解耦
缺点
- 创建订阅者本身要消耗一定的时间和内存
- 多个发布者和订阅者嵌套在一起的时候,程序难以跟踪维护
用途
- node.js EventEmitter 中的 on 和 emit 方法
- Vue 中的 emit 方法
- 微信公众号、b 站关注后,当有文章发布后,关注的人可以接收到消息推送
- 等等
经典的发布订阅模式实现
const eventEmitter = {
list: {},
on: function (event, fn) {
if (!this.list[event]) {
this.list[event] = [];
}
this.list[event].push(fn);
return this;
},
emit: function (...args) {
const _this = this;
const event = args[0],
fns = _this.list[event];
if (!fns || fns.length === 0) {
return false;
}
fns.forEach((fn) => {
fn.apply(_this, args.slice(1, args.length));
});
return _this;
},
};
function fans1(content) {
console.log('粉丝1收到了:', content);
}
function fans2(content) {
console.log('粉丝2收到了:', content);
}
// 订阅
eventEmitter.on('up', fans1);
eventEmitter.on('up', fans2);
// 发布
eventEmitter.emit('up', '发布订阅模式');
增强版
补充了 once 和 off 方法。
const eventEmitter = {
list: {},
on(event, fn) {
if (!this.list[event]) {
this.list[event] = [];
}
this.list[event].push(fn);
return this;
},
emit(...args) {
const _this = this;
const event = args[0],
fns = _this.list[event];
if (!fns || fns.length === 0) {
return false;
}
fns.forEach((fn) => {
fn.apply(_this, args.slice(1, args.length));
});
return _this;
},
// 取消订阅
off(event, fn) {
const fns = this.list[event];
if (!fns) return false;
if (!fn) {
fns.length = 0;
} else {
let cb;
for (let i = 0, len = fns.length; i < len; i++) {
cb = fns[i];
if (cb === fn || cb.fn === fn) {
fns.splice(i, 1);
break;
}
}
}
return this;
},
// 监听一次
once(event, fn) {
const _this = this;
function on(...args) {
_this.off(event, on);
fn.apply(_this, args);
}
on.fn = fn;
_this.on(event, on);
return _this;
},
};
function fans1(content) {
console.log('粉丝1收到了:', content);
}
function fans2(content) {
console.log('粉丝2收到了:', content);
}
// 订阅
eventEmitter.on('up', fans1);
eventEmitter.off('up', fans1);
eventEmitter.once('up', fans2);
// 发布
eventEmitter.emit('up', '发布订阅模式');
eventEmitter.emit('up', '发布订阅模式');
Vue 中的实现
在 eventsMixin 中挂载到 Vue 构造函数的 prototype 中
vm.$on
将回调 fn 注册到事件列表中即可,_events 在实例初始化时创建。
Vue.prototype.$on = function (event, fn) {
const vm = this;
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn);
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn);
}
return vm;
};
vm.$off
支持off
、off('eventName')
、off('eventName', fn)
、off(['eventName1', 'eventName2'])
、off(['eventName1', 'eventName2'], fn)
多种情况
Vue.prototype.$off = function (event, fn) {
const vm = this;
if (!arguments.length) {
vm._events = Object.create(null);
return vm;
}
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$off(event[i], fn);
}
return vm;
}
const cbs = vm._events[event];
if (!cbs) {
return vm;
}
if (!fn) {
vm._events[event] = null;
return vm;
}
if (fn) {
const cbs = vm._events[event];
let cb;
let i = cbs.length;
while (i--) {
cb = cbs[i];
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1);
break;
}
}
}
return vm;
};
vm.$once
先移除事件监听,再执行函数。
Vue.prototype.$once = function (event, fn) {
const vm = this;
function on() {
vm.$off(event, on);
fn.apply(vm, arguments);
}
on.fn = fn;
vm.$on(event, on);
return vm;
};
vm.$emit
取出对应 event 回调函数列表,再遍历执行
Vue.prototype.$emit = function (event) {
const vm = this;
let cbs = vm._events[event];
if (cbs) {
const args = Array.from(arguments).slice(1);
for (let i = 0, l = cbs.length; i < l; i++) {
try {
cbs[i].apply(vm, args);
} catch (e) {
console.error(e, vm, `event handler for "${event}"`);
}
}
}
return vm;
};