观察者模式也可以称之为监听者模式。

它由观察者「Observer」与被观察者「Subject」共同组成。

INFO

又名发布订阅模式,在复杂场景下,为了适应更多需求,除了观察者与被观察者,还会引入更多的角色,因此复杂场景下,大家更愿意称之为发布订阅模式。

我们会遇到很多这种场景,例如事件监听。当我们点击按钮时,对应按钮如果添加了点击事件监听,那么对应的逻辑就会自动执行。而不需要我们显示的调用该逻辑去执行他。

我们也可以自定义事件。

code.ts
1
var event = new Event('build')
2
3
// 添加观察者
4
document.addEventListener('build', function () {
5
console.log('我是新增的一个观察者1,我现在观察到 document 触发了 build 事件')
6
})
7
8
// 添加观察者
9
document.addEventListener('build', function () {
10
console.log('我是新增的一个观察者2,我现在观察到 document 触发了 build 事件')
11
})
12
13
// 被观察者触发事件
14
document.dispatchEvent(event)

观察者模式解决的就是这样的问题。当某一个条件满足要求或者触发某一种事件时,所有的观察者都会感知到并执行对应的逻辑。

在前端里最常见的就是 React/Vue 的数据思维:当我们改变 state/data 里的数据时,就会自动渲染 UI。

上图就是 Vue 的内部原理,观察者 Observer 观察数据 Data 的变化,如果一旦发现数据产生了变化就会通知后续的流程,最终完成 View 的更新。

1

基本实现

代码逻辑比较简单,直接上代码。

code.ts
1
let subjectid = 0
2
let observerid = 0
3
4
class Subject {
5
constructor(name) {
6
// 观察者队列
7
this.observers = []
8
this.id = subjectid++
9
this.name = name
10
}
11
12
// 添加观察者
13
addListener(observer) {
14
this.observers.push(observer)
15
}
16
17
// 删除观察者
18
removeListener(observer) {
19
this.observers = this.observers.filter(item => item.id !== observer.id)
20
}
21
22
// 通知
23
dispatch() {
24
this.observers.forEach(item => {
25
item.watch(this.name)
26
})
27
}
28
}
29
30
class Observer {
31
constructor() {
32
this.id = observerid++
33
}
34
watch(subjectName) {
35
console.log(`观察者${this.id}发现了被观察者 ${subjectName} 产生了变化。`)
36
}
37
}
38
39
const sub = new Subject('div元素')
40
const observer1 = new Observer()
41
const observer2 = new Observer()
42
43
sub.addListener(observer1)
44
sub.addListener(observer2)
45
46
sub.dispatch()

被观察者,通过 addListener 添加观察者。

当被观察者发生变化时,调用 sub.dispatch 通知所有观察者。

观察者通过 watch 接收到通知之后,执行预备好的逻辑。

以 DOM 元素事件绑定为例,我们进行一个类比

code.ts
1
div.addEventListener('click', fn, false)
2
div.addEventListener('mousemove', fn, false)
3
div.addEventListener('mouseup', fn, false)

此时,被观察者,就是 div 元素。

没有明确的观察者,而是直接传入回调函数 fn,事件触发时,回调函数全部执行。因此也可以将此处的回调函数理解为观察者。

这种以回调函数作为观察者的方式更符合事件绑定机制。

此处还需要传入一个字符串用于区分事件类型。这种方式又应该怎么实现呢?

思考一下,直接上代码

code.ts
1
class Subject {
2
constructor(name) {
3
// 观察者队列
4
// 格式为: { click: [fn1, fn2], scroll: [fn1, fn2] }
5
this.events = {}
6
this.name = name
7
}
8
9
// 添加观察者
10
addListener(type, fn) {
11
const cbs = this.events[type]
12
if (cbs && cbs.length > 0) {
13
const _cbs = cbs.filter(cb => cb != fn)
14
_cbs.push(fn)
15
this.events[type] = _cbs
16
} else {
17
this.events[type] = [fn]
18
}
19
}
20
21
// 删除观察者
22
removeListener(type) {
23
delete this.events[type]
24
}
25
26
// 通知
27
dispatch(type) {
28
this.events[type].forEach(cb => cb())
29
}
30
}
31
32
const sub = new Subject('div')
33
34
sub.addListener('build', function() {
35
console.log('build 事件触发1')
36
})
37
sub.addListener('build', function () {
38
console.log('build 事件触发2')
39
})
40
sub.addListener('click', function() {
41
console.log('click 事件触发')
42
})
43
44
sub.dispatch('build')

2

Vue 的实现原理

Vue 的核心是监听 data 中的数据变化,然后让数据的变化自动响应到 View 的变化。

由于 Vue 的场景更为复杂,因此在这个过程中引入了许多其他的角色。

Data:数据,最初的被观察者

Observer:Data 的观察者,数据劫持,监听数据的变化

Dep:订阅器,收集 Watcher。

Watcher:Dep 的订阅者,收到通知之后更新试图

数据的变化,会被 Observer 劫持,并且通知订阅器 Dep。Dep 收到通知之后,又会通知 Watcher,Watcher 收到属性的变化通知并执行响应的函数去更新试图 View。

Observer

data 数据的监听机制不需要我们去实现,因为在 JavaScript 中,对象中的每一个属性都具备自身的描述对象 descriptor

当访问数据时,描述对象中 get 方法会执行。

当修改数据时,描述对象中的 set 方法会执行。

因此,我们只需要利用这个机制,去监听数据即可。核心的方法是 JavaScript 提供的 Object.defineProperty

此处我们使用递归的方式,监听数据的所有属性。

与此同时,还需要关注的一个点是,我们需要

在 get 执行时,让 Dep 收集 Watcher。

在 set 执行时,通知 Watcher 执行更新逻辑。

因此,完整的实现如下:

code.ts
1
class Observer {
2
constructor(data) {
3
this.data = data;
4
this.walk(data);
5
}
6
walk(data) {
7
Object.keys(data).forEach((key) => {
8
this.defineReactive(data, key, data[key]);
9
});
10
}
11
defineReactive(data, key, val) {
12
const dep = new Dep();
13
Object.defineProperty(data, key, {
14
enumerable: true,
15
configurable: true,
16
get: () => {
17
if (Dep.target) {
18
dep.addSub(Dep.target);
19
}
20
return val;
21
},
22
set: function (newVal) {
23
if (newVal === val) {
24
return;
25
}
26
val = newVal;
27
dep.notify();
28
}
29
});
30
}
31
}

使用时,可以直接 new,也可以增加一个额外的判断

code.ts
1
function observe(value, vm) {
2
if (!value || typeof value !== 'object') {
3
return;
4
}
5
return new Observer(value);
6
};

Dep

此处的 Dep,也是一个被观察者,它与 Watcher 也是被观察者与观察者的关系。因此 Dep 的实现也非常简单,作用就是收集观察者 Watcher,以及通知 Watcher 更新。

code.ts
1
class Dep {
2
constructor() {
3
this.subs = []
4
}
5
addSub(sub) {
6
this.subs.push(sub);
7
}
8
notify() {
9
this.subs.forEach(function (sub) {
10
sub.update();
11
});
12
}
13
}

Watcher

对于订阅者 Watcher 一个非常重要的思考就是何时将自己添加到 Dep 中。

我们刚才在 Observer 的实现中,已经明确好了时机,也就是在访问数据时,可以将 Watcher 添加进去。

因此,我们在初始化 Watcher 时,可以手动去访问一次 data 中的数据,强制触发 get 执行。这样我们就可以在 get 的逻辑中,将 Watcher 添加到 Dep 里了。

code.ts
1
class Watcher {
2
constructor(vm, exp, cb) {
3
this.vm = vm;
4
this.exp = exp;
5
this.cb = cb;
6
this.value = this.get();
7
}
8
update() {
9
this.run()
10
}
11
run() {
12
var value = this.vm.data[this.exp];
13
var oldVal = this.value;
14
if (value !== oldVal) {
15
this.value = value;
16
this.cb.call(this.vm, value, oldVal);
17
}
18
}
19
get() {
20
Dep.target = this;
21
// 访问data,触发 get 执行,把当前的 Watcher 实例,添加到 Dep 中
22
var value = this.vm.data[this.exp]
23
// 添加成功之后,释放掉自身,其他的实例还需要该引用
24
Dep.target = null;
25
return value;
26
}
27
}

Vue

最后,我们需要将 data,Observer,Watcher 关联起来,形成一个整体。

code.ts
1
class Vue {
2
constructor(options, el, exp) {
3
this.data = options.data;
4
5
// 劫持 data
6
observe(this.data);
7
8
// 初始化显示
9
el.innerHTML = this.data[exp];
10
11
// 创建 Watcher 实例
12
new Watcher(this, exp, function (value) {
13
el.innerHTML = value;
14
});
15
return this;
16
}
17
}

这样,我们就可以 new 一个 Vue 对象,然后通过 vue.data.text = 'xxx' 去改变 View 的显示了。

我们也可以对 Vue 的数据做一个代理处理,让 vue.data.textvue.text 的操作是等价的。

code.ts
1
class Vue {
2
constructor(options, el, exp) {
3
this.data = options.data;
4
Object.keys(this.data).forEach((key) => {
5
this.proxyKeys(key);
6
});
7
8
// 劫持 data
9
observe(this.data);
10
11
// 初始化显示
12
el.innerHTML = this.data[exp];
13
new Watcher(this, exp, function (value) {
14
el.innerHTML = value;
15
});
16
return this;
17
}
18
proxyKeys(key) {
19
Object.defineProperty(this, key, {
20
enumerable: false,
21
configurable: true,
22
get: () => {
23
return this.data[key];
24
},
25
set: (newVal) => {
26
this.data[key] = newVal;
27
}
28
});
29
}
30
}

封装好之后,最后的使用代码就很简单了。

code.ts
1
var ele = document.querySelector('#wrap');
2
var vue = new Vue({
3
data: {
4
text: 'hello world'
5
}
6
}, ele, 'text');
7
8
document.addEventListener('click', function() {
9
vue.data.text = `${vue.data.text} vue click.`
10
}, false)
专栏首页
到顶
专栏目录