观察者模式与发布订阅模式

关于观察者模式与发布/订阅模式,不少大神都有帖子对他们做出了解释,但是很多文章都将两者混在了一起,认为他们就是同一种模式,实际上这两者还是有些差异的,所以本文就从我在谷歌的查阅和个人的理解,来仔细讲讲这两种模式,已经他们的一些应用场景。

观察者模式

官方给出的观察者模式的解释是这样的:

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

观察者模式实现的,其实就是当目标对象的某个属性发生了改变,所有依赖着目标对象的观察者都将接到通知,做出相应动作。
所以在目标对象的抽象类里,会保存一个观察者序列。当目标对象的属性发生改变生,会从观察者队列里取观察者调用各自的方法。

优点

  • 观察者和被观察者是抽象耦合的。
  • 建立一套触发机制。

缺点

  • 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
  • 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

下面通过一张图来看一下观察者模式的实现。

Alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class Subject {
let observers = [];
let state;

getState() {
return this.state;
}

setState(state) {
this.state = state;
notifyAllObservers();
}

attach(observer){
observers.push(observer);
}

notifyAllObservers(){
for (observer in observers) {
observer.update();
}
}
}

class Observer {
let subject;
update();
}

class BinaryObserver extends Observer {
constructor(subject) {
super();
subject.attach(this);
}
update() {
console.log("Binary");
}
}

class OctalObserver extends Observer {
constructor(subject) {
super();
subject.attach(this);
}
update() {
console.log("Octal");
}
}

var subject = new Subject();
var binaryObserver = new BinaryObserver(subject);
var octalObserver = new OctalObserver(subject);

subject.setState(15);
//Binary
//Octal

发布/订阅模式

在很多文章里讲到的观察者模式,其实说的都是发布订阅模式,那么他们的差别到底在哪里呢,让我们一点点往下看。
维基中对于发布/订阅是这样描述的:

发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

也就是说,发布/订阅模式和观察者最大的差别就在于消息是否通过一个中间类进行转发。

优点

  • 相较于观察者模式,发布/订阅发布者和订阅者的耦合性更低
  • 通过并行操作,消息缓存,基于树或基于网络的路由等技术,发布/订阅提供了比传统的客户端–服务器更好的可扩展性

缺点

  • 当中间类采用定时发布通知时,使用发布订阅无法确定所有订阅者是否都成功收到通知
  • 当负载激增,请求订阅的订阅者数量增加,每个订阅者接收到通知的速度将会变慢

两种模式的区别

由上,我们就可以得出这两者的区别了:

  • 发布/订阅模式相比于观察者模式多了一个中间媒介,因为这个中间媒介,发布者和订阅者的关联更为松耦合
  • 观察者模式通常用于同步的场景,而发布/订阅模式大多用于异步场景,例如消息队列。

Alt text

到这里,肯定会有小伙伴问,为什么没有发布/订阅模式的代码实例。其实在很多JS框架中,都采用发布/订阅模式进行了不少设计,下面我们就从Vue和Node来深入讲一讲关于发布/订阅的使用。

Vue中的发布/订阅设计

Vue中使用到发布/订阅模式最经典的两块实现就是数据双向绑定父子组件通信

数据双向绑定

vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。
具体实现数据双向绑定会需要三个步骤:

  • 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
  • 实现一个订阅者Watcher,每一个Watcher都绑定一个更新函数,watcher可以收到属性的变化通知并执行相应的函数,从而更新视图。
  • 实现一个解析器Compile,可以扫描和解析每个节点的相关指令(v-model,v-on等指令),如果节点存在v-model,v-on等指令,则解析器Compile初始化这类节点的模板数据,使之可以显示在视图上,然后初始化相应的订阅者(Watcher)。

Alt text

数据劫持

Vue中,利用 Object.defineProperty() 实现数据劫持,监听到数据的变化。

1
2
3
4
5
6
7
8
Object.defineProperty(data, key, {
set: function (value) {
//...
},
get: function () {
//...
}
})

实现Observer

Observer是一个数据监听器,用来监听所有的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
function Observer(data) {
this.data = data;
this.walk(data);
}

Observer.prototype = {
walk: function(data) {
var self = this;
//遍历对象,获得对象所有属性的监听
Object.keys(data).forEach(function(key) {
self.defineReactive(data, key, data[key]);
});
},
defineReactive: function(data, key, val) {
var dep = new Dep();
// 递归遍历所有子属性
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function getter () {
if (Dep.target) {
// 在这里添加一个订阅者,有关Dep.target的获得,会在watcher中实现
dep.addSub(Dep.target);
}
return val;
},
// setter,如果对一个对象属性值改变,就会触发setter中的dep.notify(),通知watcher(订阅者)数据变更,执行对应订阅者的更新函数,来更新视图。
set: function setter (newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
dep.notify();
}
});
}
};

function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
};

// 消息订阅器Dep,订阅器Dep主要负责收集订阅者,然后在属性变化的时候执行对应订阅者的更新函数
function Dep () {
this.subs = [];
}
Dep.prototype = {
/**
* [订阅器添加订阅者]
* @param {[Watcher]} sub [订阅者]
*/
addSub: function(sub) {
this.subs.push(sub);
},
// 通知订阅者数据变更
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
Dep.target = null;

实现Watcher

watcher就是一个订阅者,里面包含了添加订阅者到消息队列和接收响应发布者的通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get(); // 将自己添加到订阅器的操作
}

Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
};

参数解释:

  • cb:订阅者绑定的更新函数。
  • vm:Vue实例化的对象。
  • exp:节点的v-model或v-on:click等指令的属性值。

关联Observer和Watcher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function SelfVue (data, el, exp) {
this.data = data;
observe(data);
el.innerHTML = this.data[exp]; // 初始化模板数据的值
new Watcher(this, exp, function (value) {
el.innerHTML = value;
});
return this;
}


<body>
<h1 id="name">{{name}}</h1>
</body>
<script src="js/observer.js"></script>
<script src="js/watcher.js"></script>
<script src="js/index.js"></script>
<script type="text/javascript">
var ele = document.querySelector('#name');
var selfVue = new SelfVue({
name: 'hello world'
}, ele, 'name');

window.setTimeout(function () {
console.log('name值改变了');
selfVue.data.name = 'canfoo';
}, 2000);
</script>

其实到这里我们就已经实现了vue的数据双向绑定,从这个绑定过程,我们也很明确看到发布/订阅模式是如何起作用的。
本文主要围绕两种设计模式展开,有关compile解析节点的部分,在这里就不做细讲,感兴趣的小伙伴可以继续深入源码探究。

父子组件通信

Vue的父子组件通信也用到了发布/订阅模式。

  • A组件通过 $on 订阅观察特定事件
  • B组件通过 $emit 将变化广播给其他订阅观察对应事件的组件,并调用他们的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = 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
}

Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
for (let i = 0, l = cbs.length; i < l; i++) {
cbs[i].apply(vm, args)
}
}
return vm
}

Node中的发布/订阅设计

Node中有一个EventEmiter模块,其消息机制采用的就是发布/订阅思想,下面我们来手写一个EventEmiter类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class EvenEmiter{
construct() {
this._events = {};
this.defaultMaxListener = 10;
}

setMaxListner(n) {
this._maxListeners = n;
}

getMaxListener() {
return this._maxListeners ? this.maxListeners : this.defaultMaxListeners;
}

once(eventName, callback) {
wrap(...args) {
callback(...args);
this.removeListener(eventName,callback);
}
wrap.cb = callback;
this.on(eventName, wrap);
}

on(eventName, callback) {
if (!this._events) {
this._events = {}
}
if (this._events[eventName]) {
this._events[eventName].push(callback);
}
else {
this._events[eventName] = [callback];
}
}

emit(eventName) {
if (this._events[eventName]) {
this._events[eventName].forEach((fn) => {
fn()
});
}
}

removeListener(eventName, callback) {
if (this._events[eventName]) {
this._events = this._events.filter(fn => {
return fn !== callback;
})
}
}

addEvnetListener(eventName, callback) {
this.on(eventName, callback);
}
}

以上就是有关观察者模式和发布/订阅模式的全部内容,如果有补充和有错的地方,欢迎大家留言。

参考链接:
vue的双向绑定原理及实现
node 订阅发布及实现