什么是发布-订阅模式
发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知
订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码
举个例子
杨幂逛淘宝店的时候,看中一双鞋子,准备下单的时候,发现该鞋子已经卖完了,商家提示说,如果你想要这双鞋子就先关注我们的店铺,我们鞋子上架后第一时间将通知您,好巧不巧的是,杨幂的闺蜜-糖糖也看中了这双鞋子,同样关注了这家店铺。在此期间店铺和杨幂、糖糖之间不用来回的沟通询问,鞋子什么时候上架,预计什么时候上架等问问,店铺只需要在鞋子上架的第一时间发送消息就行,杨幂和糖糖只需要就收消息就行
这就是典型的一个发布-订阅模式。店铺是发布者,杨幂和糖糖属于订阅者,用户将订阅的事件注册到调度中心,当店铺将鞋子上架该事件发布到调度中心,调度中心会及时发消息告知用户。
发布-订阅模式的实现
var publishObj = {}; //定义发布者
publishObj.list = []; //缓存列表, 存放订阅者回调函数
//增加订阅者
publishObj.addListen = function (fn) {
publishObj.list.push(fn);
};
// 发布消息
publishObj.publish = function () {
for (var i = 0,fn; fn=publishObj.list[i++];) {
publishObj.list[i].apply(this, arguments);
}
};
// 张三订阅了消息
publishObj.addListen(function (data) {
console.log("订阅是A款:", data);
});
// 李四订阅了消息
publishObj.addListen(function (data) {
console.log("订阅是B款:", data);
});
// 发布消息
publishObj.publish("鞋子A上架了");
publishObj.publish("鞋子B上架了");
// 结果
// 订阅是A款: 鞋子A上架了
// 订阅是B款: 鞋子A上架了
// 订阅是A款: 鞋子B上架了
// 订阅是B款: 鞋子B上架了
由上面可以看出 订阅是A款的同样收到了订阅是B款的消息,明显,我们只希望关注我们自己喜欢的东西,不喜欢的肯定是不希望收到消息提示的,所以,我们需要把每个订阅的事件加一个key,这样我们可以根据对应的key进行发布对应的消息,就不会收到无关紧要的消息。
var publishObj = {}; //定义发布者
publishObj.list = []; //缓存列表, 存放订阅者回调函数
//增加订阅者
publishObj.addListen = function (key,fn) {
(publishObj.list[key] || (publishObj.list[key] = [])).push(fn);
};
// 发布消息
publishObj.publish = function () {
const key =Array.prototype.shift.call(arguments); // 订阅消息的key
const fns = this.list[key]; //订阅的事件
// 如果没有订阅过该消息的话,则返回
if(!fns || fns.length === 0) {
return;
}
for (var i = 0,fn;fn=fns[i++];) {
fns[i].apply(this, arguments);
}
};
// 张三订阅了消息
publishObj.addListen('订阅是A款',function (data) {
console.log("订阅是A款:", data);
});
// 李四订阅了消息
publishObj.addListen('订阅是B款',function (data) {
console.log("订阅是B款:", data);
});
// 发布消息
publishObj.publish("订阅是A款","鞋子A上架了");
publishObj.publish("订阅是B款","鞋子B上架了");
// 结果
// 订阅是A款: 鞋子A上架了
// 订阅是B款: 鞋子A上架了
由此可见,改造后,只会收到自己订阅模块的消息。
上面案例都只是针对单个发布-订阅,如果我们还需要对其他的对象进行发布-订阅,那还需要封装一个整体的方法
发布-订阅实现思路
- 定义一个对象
- 在该对象上创建一个缓存事件(调度中心),存放所有的订阅事件
- on方法把所有的订阅事件都加到缓存列表中
- emit方法首先获取到参数的第一个参数:事件名,然后根据事件名找到并发布缓存列表中对应的函数
- off取消订阅,根据事件名来取消订阅
- once只订阅一次,先订阅然后取消
//定义发布者
let enevtEmit = {
list:[], //缓存列表, 存放订阅者回调函数
on:function (key,fn) { //增加订阅者
let _this = this;
(_this.list[key] || (_this.list[key] = [])).push(fn);
return _this
},
emit:function () { // 发布消息
let _this = this;
const key = Array.prototype.shift.call(arguments); // 订阅消息的key
const fns = this.list[key]; //订阅的事件
// 如果没有订阅过该消息的话,则返回
if(!fns || fns.length === 0) {
return;
}
for (var i = 0,fn; fn=fns[i++];) {
fn.apply(this, arguments);
}
return _this
}
}
function user1(data){
console.log("订阅是A款:", data);
}
function user2(data){
console.log("订阅是B款:", data);
}
enevtEmit.on('订阅是A款',user1)
enevtEmit.on('订阅是B款',user2)
// 发布消息
enevtEmit.emit("订阅是A款","鞋子A上架了1");
enevtEmit.emit("订阅是B款","鞋子B上架了1");
// 结果
// 订阅是A款: 鞋子A上架了
// 订阅是B款: 鞋子A上架了
那如果订阅之后想取消,或者你希望订阅一次,后续就不在打扰又怎么处理呢
//定义发布者
let enevtEmit = {
list: [], //缓存列表, 存放订阅者回调函数
on: function (key, fn) {
//增加订阅者
let _this = this;
(_this.list[key] || (_this.list[key] = [])).push(fn);
return _this;
},
emit: function () {
// 发布消息
let _this = this;
const key = Array.prototype.shift.call(arguments); // 订阅消息的key
const fns = this.list[key]; //订阅的事件
// 如果没有订阅过该消息的话,则返回
if (!fns || fns.length === 0) {
return;
}
for (var i = 0, fn; (fn = fns[i++]); ) {
fn.apply(this, arguments);
}
return _this;
},
off: function (key, fn) { // 取消订阅,从订阅者中找到当前的key 然后进行删掉
let _this = this;
var fns = _this.list[key];
if (!fns) {
// 没有订阅事件直接返回
return false;
}
!fn && fns && (fns.length = 0); // 不传订阅事件,意味着取消所有的订阅事件
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;
},
once:function(event,fn){ // 订阅一次,先发布一次,然后给删掉
let _this = this;
// fn.apply(_this,arguments)
// _this.off(event,fn)
function on () {
_this.off(event, on);
fn.apply(_this, arguments);
}
on.fn = fn;
_this.on(event, on);
return _this;
}
};
function user1(data) {
console.log("订阅是A款:", data);
}
function user2(data) {
console.log("订阅是B款:", data);
}
function user3(data) {
console.log("订阅是AB款:", data);
}
enevtEmit.on("订阅是A款", user1);
enevtEmit.on("订阅是B款", user2);
enevtEmit.off('订阅是A款',user1);
enevtEmit.once('订阅是AB款',user3);
// 发布消息
enevtEmit.emit("订阅是A款", "鞋子A上架了");
enevtEmit.emit("订阅是B款", "鞋子B上架了");
enevtEmit.emit('订阅是AB款','鞋子AB上架了');
enevtEmit.emit('订阅是AB款',"鞋子AB上架了");
// 结果
// 订阅是AB款: 订阅是AB款
//订阅是B款: 鞋子B上架了
总结
优点
1、支持简单的广播模式,当对象状态发生改变时,会自动通知已经订阅过的对象
2、发布者与订阅者耦合性降低,发布者只管发布一条消息出去,它不关心这条消息如何被订阅者使用,同时,订阅者只监听发布者的事件名,只要发布者的事件名不变,它不管发布者如何改变
缺点
1、创建订阅者本身要消耗一定的时间和内存
2、虽然可以弱化对象之间的联系,如果过度使用的话,反而使代码不好理解及代码不好维护等等
Vue 中的实现
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
}
观察者模式和发布订阅的区别
观察者模式
观察者模式一般至少有一个可被观察的对象 Subject ,可以有多个观察者去观察这个对象。二者的关系是通过被观察者主动建立的,被观察者至少要有三个方法——添加观察者、移除观察者、通知观察者。
当被观察者将某个观察者添加到自己的观察者列表后,观察者与被观察者的关联就建立起来了。此后只要被观察者在某种时机触发通知观察者方法时,观察者即可接收到来自被观察者的消息。
发布订阅模式
与观察者模式相比,发布订阅核心基于一个中心来建立整个体系。其中发布者和订阅者不直接进行通信,而是发布者将要发布的消息交由中心管理,订阅者也是根据自己的情况,按需订阅中心中的消息
观察者模式代码实现
class Subject {
constructor() {
this.observerList = [];
}
addObserver(observer) {
this.observerList.push(observer);
}
removeObserver(observer) {
const index = this.observerList.findIndex(
(o) => o.name === observer.name
);
this.observerList.splice(index, 1);
}
notifyObservers(message) {
const observers = this.observerList;
observers.forEach((observer) => observer.notified(message));
}
}
class Observer {
constructor(name, subject) {
this.name = name;
if (subject) {
subject.addObserver(this);
}
}
notified(message) {
console.log(this.name, "got message", message);
}
}
const subject = new Subject();
const observerA = new Observer('observerA',subject);
const observerB = new Observer('observerB');
subject.addObserver(observerB);
subject.notifyObservers('Hello from subject');
subject.removeObserver(observerA);
subject.notifyObservers('Hello again');
// 结果
// observerA got message Hello from subject
// observerB got message Hello from subject
// observerB got message Hello again