一、前言
一天,小猪佩奇去了一家西餐厅,点了一份西冷牛扒,还叫了圣女果。后来服务员上了一碟番茄:佩奇小姐,这是你的「圣女果」。佩奇猪一眼就看出了猫腻:这tm是番茄,不是圣女果啊!于是就跟服务员理论起来:这是番茄不是圣女果,不是!服务员一脸懵逼:番茄不就是圣女果吗?...佩奇猪一脸「黑人问号」了:番茄是菜,圣女果是水果,这能一样???
观察者模式和发布-订阅模式大概就是番茄和圣女果的关系一样,剪不断理还乱。我相信绝大多数js开发者人为两者关系是一样的。只是两种不同写法罢了。在阅读了MSN上的官方文档之前,我也是这样认为的。但是在这之后,我觉得两者差距还是有的,首先是名字。如果是两种不同写法,那为何名字不一样呢?
二、区别
先用一张图片说明两个模式的不同,左边是观察者模式,右边是发布-订阅模式。
区别
观察者模式
从图中可以看出来,一个对象(subject)维持一群依赖它的对象(subject)。从数学的角度来讲,这是一种一对多的关系。在这个模式里,观察者(Observer)直接订阅(subscribe)主题(Subject),而当主题被激活的时候,会触发(fire)观察者里的事件。
发布-订阅模式
可以看出,观察者模式中,观察者和主题两个对象互相依赖着,没有解耦。在发布-订阅模式中在两者之中多了一层Event Bus,以此来解耦观察者和主题。
注:事件总线也有说法叫为调度中心。本质上是一样的。不过因为写Vue时候习惯用Event Bus来说了,所以本文的调度中心皆以事件总线称呼。
两者最主要的区别就是,有没有一层中间的事件总线将两者解耦。大多数js开发者认为两种模式其实是一种模式两种写法的原因大概是它们都实现了一个关键的功能:发布事件-订阅事件并触发。
简单用代码实现
观察者模式
function Observer(data) {
this.data = data;
}
Observer.prototype = {
notify() {
console.log('notify', this.data)
}
}
function Subject() {
this.subs = [];
this.subscribe = function(sub) {
this.subs.push(sub)
}
this.unsubscribe = function(sub) {
const index = this.subs.indexOf(sub)
if(index > -1){
this.subs.splice(index , 1)
}
}
this.fire = function() {
this.subs.forEach((sub) => {
sub.notify()
})
}
}
const o1 = new Observer('hello1')
const o2 = new Observer('hello2')
const sub1 = new Subject()
sub1.subscribe(o1)
sub1.subscribe(o2)
sub1.fire()
观察者模式在Dom中的实践
class Dom {
constructor() {
this.events = {}
}
addEventListener (event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
}
fireEvent(e) {
if (!this.events[e]) {
return
}
this.events[e].forEach((item) => {
item()
})
}
};
const dom = new Dom();
dom.addEventListener('click', () => {
console.log('onclick')
})
dom.addEventListener('scroll', () => {
console.log('onscroll')
})
dom.fireEvent('click')
dom.fireEvent('scroll')
发布-订阅模式
class EventChannel {
constructor() {
this.subject = {}
};
_hasSubject(sub) {
return this.subject[sub] ? true : false
}
on(sub, cb) {
if (!this._hasSubject(sub)) {
this.subject[sub] = []
}
this.subject[sub].push(cb)
}
emit(sub, ...data) {
if (!this._hasSubject(sub)) {
return
}
this.subject[sub].forEach((item) => {
item(...data)
})
}
}
const eventBus = new EventChannel()
const people = function(val) {
console.log('我收到了新的商品通知', val)
}
eventBus.on('newItem', people);
const merchat = function(val) {
const item = {
item: val
}
eventBus.emit('newItem', item)
}
merchat('Book')```