设计模式:观察者模式

观察者模式

使用观察者模式的一个常见的场景是: 一个观察对象需要通知一组相互独立的观察者发生变化, 并且这个观察对象和观察者之间是松耦合的关系。观察者模式是由观察者和观察对象组成的, 观察对象维护一组观察者,这组观察者依赖观察对象, 当观察对象的状态发生变化的时候, 会自动通知这组观察者发生动作。一个观察对象由属于自己的一组观察者, 观察对象的作用是注册, 删除观察者, 以及在合适的时机触发观察者。在其他书籍中对于观察者模式的一些定义如下:
"One or more observers are interested in the state of a subject and register their interest with the subject by attaching themselves. When something changes in our subject that the observer may be interested in, a notify message is sent which calls the update method in each observer. When the observer is no longer interested in the subject's state, they can simply detach themselves."
在观察者模式中, 存在下面四种组成部分:
  • 观察对象:维护一组观察者, 可以新增 / 删除观察者, 通知观察者发生变化的逻辑
  • 观察者: 提供一个当观察者对象状态发生变化的时候进行状态变化的一个接口
  • 观察对象实例: 当相关状态发生变化的时候通知观察者,
  • 观察者实例: 具体化一个数据改变的接口, 这个观察者实例是和观察对象相关联的
下面是一个观察者模式的模型:
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
// 一组观察者, 扩展了一些删除, 添加观察者的一些方法
class ObserverList {
constructor () {
this.observers = [];
}
append(ob) {
this.observers.push(ob);
}
delete(obIndex) {
this.observers.splice(obIndex, 1);
}
get(obIndex) {
return this.observers[obIndex];
}
clear() {
this.observers = [];
}
count() {
return this.observers.length;
}
getList() {
return this.observers;
}
}

// 观察者, 提供了一个 update 接口
class Observer {
constructor (cb) {
this.cb = cb;
}
update() {
this.cb();
}
}

// 观察者对象
class Subject {
constructor() {
// 维护一组观察者
this.observerList = new ObserverList();
}
add(ob) {
this.observerList.append(ob);
}
notify() {
for (let ob of this.observerList.getList()) {
ob.update();
}
}
remove(obIndex) {
this.observerList.delete(obIndex);
}
}

const subject = new Subject();
const observer = new Observer(action);

const action = () => {
console.log('hello world');
};

// 注册观察者
subject.add(observer);

// 通知与之依赖的观察者动作
subject.notify();
上面的观察者模式, 其实可以使用下面的图进行说明: 在上面的代码中, 我们可以发现, 观察者和观察者对象之间是松耦合的, 观察者对象维护者一组观察者, 观察者对象需要做的是通知相关观察者进行更新。

和发布/订阅模式的区别

上面的代码使用发布/订阅模式实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class EventMananger {
constructor() {
this.events = {};
}
publish(event) {
if (!this.events[event]) return false;
let eventCount = this.events[event].length;
while (eventCount--) {
this.events[event][eventCount]();
}
}
subscribe(event, fn) {
(this.events[event] || (this.events[event] = [])).push(fn);
}
}

const mananger = new EventMananger();

mananger.subscribe('action', action);

mananger.publish('action');
两者区别:
  1. 对于观察者模式, 其订阅和发布消息是在同一个对象上面进行的(subject), 对于发布订阅模式, 其角色有三种, 订阅者, 发布者 以及 中间存放方法的事件列表。发布者向事件列表中推送消息, 订阅者从事件列表中订阅消息, 并且当发布者推送消息的时候来触发订阅的事件。
  2. 发布订阅模式发布者和订阅者之间不存在耦合关系, 对于观察者模式, 观察者依赖观察对象, 这两者之间是松耦合关系
  3. 发布订阅模式应用于跨应用的情况下, 当多个应用之间进行通信的时候, 可以使用这种模式是实现通信, 对于 观察者模式, 主要应用于单个应用的情况。
一些缺点:使用观察者模式存在着一些缺点:
  • 观察对象不知晓观察者的状态, 当某个观察者中包含有一些自增代码的时候, 调用 观察对象的 update 方法,可能会造成观察者状态的重复更新。

两种方式在 vue 中的应用:

在 vue 源码中的 $emit $on 方法中使用了发布/订阅模式:vm._events 作为事件通道, 存放函数列表。$on订阅者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue.prototype.$on = function (event, fn) {
var vm = this;
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
} else {
// 将相关函数存入到 vm._events 中
// vm._events 作为事件列表方法事件
(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
};
$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
25
26
Vue.prototype.$emit = function (event) {
var vm = this;
{
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;
var args = toArray(arguments, 1);
var info = "event handler for \"" + event + "\"";
// 触发相应的函数方法
for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
return vm
};
在 vue 的依赖收集过程中, 使用了观察者模式: 在 vue 源码中, 初始化data 的时候, vue 会通过一个 defineReactive 函数, 这个函数是实现当数据更新时相关依赖进行更新的关键, 这个函数里面对于数据的 gettersetter 函数内进行了一些处理, 当触发 getter 函数(读取)的时候会进行依赖收集, 当 触发 setter 函数(设置)的时候会通知相关依赖进行更新。具体代码如下:
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
function defineReactive (obj, key, val, customSetter, shallow) {
// 这里的 new Dep() 是维护了一组观察者
// Dep 是观察者对象, 维护者一组观察者
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}

var childOb = !shallow && observe(val);
// 使用 defineProperty 对于对象进行拦截处理
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
// 收集依赖
// traget 全局变量
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
// 通知依赖发生变化
dep.notify();
}
});
}
这里 dep 是 用来维护者一组的观察者, 通过 dep.depend 方法收集到依赖, 然后通过 dep.notify() 来通知变动。具体到 Dep 的类:
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
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;

constructor () {
this.id = uid++
// 维护一组观察者 subs 内部存放了 Watcher 类的实例
this.subs = []
}
// 新增观察者
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除观察者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// Dep.target 是一个 watcher 实例, 这里调用的 addDep 方法是调用在 watcher 上面的方法
// 在 watcher 方法上面的 addDep 方法也是调用了 addSub 方法
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 用来通知这组观察者进行更新, 调用观察者中的 update 方法来更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
// 对于改观察者对象下面的每一个观察者都进行数据的更新处理
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
注: depend 方法中的 addDep 方法引用的是 Watcher 实例上面的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
这些观察者是 Watcher 的实例, Watcher 位于源码中的 Watcher.js 文件中, 一些更新观察者信息的方法作为实例方法。在 Watcher 的类中, 上面定义了update 的方法:
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
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
// 执行run 函数
this.run()
} else {
queueWatcher(this)
}
}

/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
// 执行 this.cb 回调
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

tips

  1. 可以在数组中查找到对象, 或者说, js 的数组中存放的是对象的引用:
    1
    2
    3
    4
    let a = {};
    let arr = [{}, a];
    arr.indexOf(a);
    // 1