Skip to content

Commit f577a83

Browse files
committed
✨ feat: 添加观察者模式
1 parent 484afa0 commit f577a83

File tree

4 files changed

+485
-0
lines changed

4 files changed

+485
-0
lines changed

Observer/Observer.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// 报社类
2+
class Press {
3+
constructor(name) {
4+
this.name = name;
5+
this.subscribers = []; //此处存放订阅者名单
6+
}
7+
8+
deliver(news) {
9+
let press = this;
10+
// 循环订阅者名单中所有的订报人,为他们发布内容
11+
press.subscribers.map(item => {
12+
item.getNews(news, press); // 向每个订阅者发送新闻
13+
})
14+
// 实现链式调用
15+
return this;
16+
}
17+
}
18+
19+
// 订报人类
20+
class Subscriber {
21+
constructor(name) {
22+
this.name = name;
23+
}
24+
25+
// 获取新闻
26+
getNews(news, press) {
27+
console.log(`${this.name} 获取来自 ${press.name} 的新闻: ${news}`)
28+
}
29+
// 订阅方法
30+
subscribe(press) {
31+
let sub = this;
32+
// 避免重复订阅
33+
if(press.subscribers.indexOf(sub) === -1) {
34+
press.subscribers.push(sub);
35+
}
36+
// 实现链式调用
37+
return this;
38+
}
39+
40+
// 取消订阅方法
41+
unsubscribe(press) {
42+
let sub = this;
43+
press.subscribers = press.subscribers.filter((item) => item !== sub);
44+
return this;
45+
}
46+
}
47+
48+
let press1 = new Press('报社一')
49+
let press2 = new Press('报社二')
50+
let press3 = new Press('报社三')
51+
52+
let sub1 = new Subscriber('订报人一')
53+
let sub2 = new Subscriber('订报人二')
54+
55+
// 订报人一订阅报社一、二
56+
sub1.subscribe(press1).subscribe(press2);
57+
// 订报人二订阅报社二、三
58+
sub2.subscribe(press2).subscribe(press3);
59+
60+
// 报社一发出新闻
61+
press1.deliver('今天天气晴');
62+
// 订报人一 获取来自 报社一 的新闻: 今天天气晴
63+
64+
65+
// 报社二发出新闻
66+
press2.deliver('今晚12点苹果发布会');
67+
// 订报人一 获取来自 报社二 的新闻: 今晚12点苹果发布会
68+
// 订报人二 获取来自 报社二 的新闻: 今晚12点苹果发布会
69+
70+
// 报社三发出新闻
71+
press3.deliver('报社二即将倒闭,请大家尽快退订');
72+
// 订报人二 获取来自 报社三 的新闻: 报社二即将倒闭,请大家尽快退订
73+
74+
// 订报人二退订
75+
sub2.unsubscribe(press2);
76+
77+
press2.deliver('本报社已倒闭');
78+
// 订报人一 获取来自 报社二 的新闻: 本报社已倒闭

Observer/Observer.md

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
# 观察者模式 (发布-订阅模式)
2+
3+
> 观察者模式也称作 发布—订阅模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,依赖于它的对象都将得到通知。在JavaScript开发中,我们一般用事件模型来替代传统的发布—订阅模式。
4+
5+
总的来说,这种模式的实质就是你可以对程序中的某个对象的状态进行观察,并且在其发生改变时能够得到通知。
6+
7+
当前已经有了很多实用观察者模式的例子,例如 `DOM事件绑定`就是一个非常典型的*发布—订阅模式*,还有 Vue.js 框架中的`数据双向绑定`,也利用了*观察者模式*
8+
9+
所以一般观察者模式有两种角色:
10+
11+
- 观察者 (发布者)
12+
- 被观察者 (订阅者)
13+
14+
下面我们举一个具体的例子,假设有三个报纸出版社,报社一,报社二,报社三,有两个订报人,分别是:订阅者1,订阅者2.此时出版社就是被观察者,订报人就是观察者。
15+
16+
我们先定义`报社类`:
17+
18+
```javascript
19+
// 报社类
20+
class Press {
21+
constructor(name) {
22+
this.name = name;
23+
this.subscribers = []; //此处存放订阅者名单
24+
}
25+
26+
deliver(news) {
27+
let press = this;
28+
// 循环订阅者名单中所有的订报人,为他们发布内容
29+
press.subscribers.map(item => {
30+
item.getNews(news, press); // 向每个订阅者发送新闻
31+
})
32+
// 实现链式调用
33+
return this;
34+
}
35+
}
36+
37+
```
38+
39+
接着我们定义`订报人类`
40+
41+
```javascript
42+
// 订报人类
43+
class Subscriber {
44+
constructor(name) {
45+
this.name = name;
46+
}
47+
48+
// 获取新闻
49+
getNews(news, press) {
50+
console.log(`${this.name} 获取来自 ${press.name} 的新闻: ${news}`)
51+
}
52+
// 订阅方法
53+
subscribe(press) {
54+
let sub = this;
55+
// 避免重复订阅
56+
if(press.subscribers.indexOf(sub) === -1) {
57+
press.subscribers.push(sub);
58+
}
59+
// 实现链式调用
60+
return this;
61+
}
62+
63+
// 取消订阅方法
64+
unsubscribe(press) {
65+
let sub = this;
66+
press.subscribers = press.subscribers.filter((item) => item !== sub);
67+
return this;
68+
}
69+
}
70+
```
71+
72+
之后我们通过实际操作进行演示:
73+
74+
```javascript
75+
let press1 = new Press('报社一')
76+
let press2 = new Press('报社二')
77+
let press3 = new Press('报社三')
78+
79+
let sub1 = new Subscriber('订报人一')
80+
let sub2 = new Subscriber('订报人二')
81+
82+
// 订报人一订阅报社一、二
83+
sub1.subscribe(press1).subscribe(press2);
84+
// 订报人二订阅报社二、三
85+
sub2.subscribe(press2).subscribe(press3);
86+
87+
// 报社一发出新闻
88+
press1.deliver('今天天气晴');
89+
// 订报人一 获取来自 报社一 的新闻: 今天天气晴
90+
91+
92+
// 报社二发出新闻
93+
press2.deliver('今晚12点苹果发布会');
94+
// 订报人一 获取来自 报社二 的新闻: 今晚12点苹果发布会
95+
// 订报人二 获取来自 报社二 的新闻: 今晚12点苹果发布会
96+
97+
// 报社三发出新闻
98+
press3.deliver('报社二即将倒闭,请大家尽快退订');
99+
// 订报人二 获取来自 报社三 的新闻: 报社二即将倒闭,请大家尽快退订
100+
101+
// 订报人二退订
102+
sub2.unsubscribe(press2);
103+
104+
press2.deliver('本报社已倒闭');
105+
// 订报人一 获取来自 报社二 的新闻: 本报社已倒闭
106+
```
107+
108+
上文我们提到了,`Vue.js`的双向绑定的原理是数据劫持和发布订阅,我们可以自己来实现一个简单的数据双向绑定
109+
110+
首先我们需要有一个页面结构
111+
112+
```
113+
<div id="app">
114+
<h3>数据的双向绑定</h3>
115+
<div class="cell">
116+
<div class="text" v-text="myText"></div>
117+
<input class="input" type="text" v-model="myText" >
118+
</div>
119+
</div>
120+
```
121+
122+
接着我们创建一个类`aVue`
123+
124+
```
125+
class aVue {
126+
constructor (options) {
127+
// 传入的配置参数
128+
this.options = options;
129+
// 根元素
130+
this.$el = document.querySelector(options.el);
131+
// 数据域
132+
this.$data = options.data;
133+
134+
// 保存数据model与view相关的指令,当model改变时,我们会触发其中的指令类更新
135+
this._directives = {};
136+
// 数据劫持,重新定义数据的 set 和 get 方法
137+
this._obverse(this.$data);
138+
// 解析器,解析模板指令,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者
139+
// 一旦数据发生变动,收到通知,更新视图
140+
this._complie(this.$el);
141+
}
142+
// 对数据进行处理,重写set和get方法
143+
_obverse(data) {
144+
let val;
145+
// 进行遍历
146+
for(let key in data) {
147+
// 判断是否属于自身的属性
148+
if(data.hasOwnProperty(key)) {
149+
this._directives[key] = [];
150+
}
151+
152+
val = data[key];
153+
if( typeof val === 'object') {
154+
// 递归遍历
155+
this._obverse(val);
156+
}
157+
158+
// 初始化当前数据的执行队列
159+
let _dir = this._directives[key];
160+
161+
// 重新定义数据的set 和 get 方法
162+
Object.defineProperty(this.$data, key, {
163+
// 可枚举的
164+
enumerable: true,
165+
// 可改的
166+
configurable: true,
167+
get: () => val,
168+
set: (newVal) => {
169+
if (val !== newVal) {
170+
val = newVal;
171+
// 触发_directives 中绑定的Watcher类更新
172+
_dir.map(item => {
173+
item._update();
174+
})
175+
}
176+
}
177+
})
178+
}
179+
}
180+
181+
// 解析器,绑定节点,添加数据的订阅者,更新视图变化
182+
_complie(el) {
183+
// 子元素
184+
let nodes = el.children;
185+
for(let i = 0; i < nodes.length; i++) {
186+
let node = nodes[i];
187+
// 递归对所有元素进行遍历
188+
if (node.children.length) {
189+
this._complie(node);
190+
}
191+
192+
// 如果有 v-text 指令, 监控 node 的值,并及时更新
193+
if (node.hasAttribute('v-text')) {
194+
let attrValue = node.getAttribute('v-text');
195+
// 将指令对应的执行方法放入指令集
196+
this._directives[attrValue].push(new Watcher('text', node, this, attrValue, 'innerHTML'))
197+
}
198+
199+
// 如果有 v-model 属性,并且元素是 input 或者 textarea 我们监听input事件
200+
if( node.hasAttribute('v-model') && ( node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
201+
let _this = this;
202+
// 添加input事件
203+
node.addEventListener('input', (function(){
204+
let attrValue = node.getAttribute('v-model');
205+
// 初始化复制
206+
_this._directives[attrValue].push(new Watcher('input', node, _this, attrValue, 'value'));
207+
return function () {
208+
// 后面每次都会更新
209+
_this.$data[attrValue] = node.value;
210+
}
211+
})())
212+
}
213+
}
214+
}
215+
}
216+
217+
```
218+
219+
`_observe`方法处理传入的data,重新改写data的`set``get`方法,保证我们可以跟踪到data的变化。
220+
221+
`_compile`方法本质是一个解析器,他通过解析模板指令,将每个指令对应的节点绑定更新函数,并添加监听数据的订阅者,数据发生变化时,就去更新视图变化。
222+
223+
接着我们定义订阅者类
224+
225+
```
226+
class Watcher{
227+
/*
228+
* name 指令名称
229+
* el 指令对应的DOM元素
230+
* vm 指令所属的aVue实例
231+
* exp 指令对应的值,本例为"myText"
232+
* attr 绑定的属性值,本例为"innerHTML"
233+
*/
234+
constructor(name, el, vm, exp, attr) {
235+
this.name = name;
236+
this.el = el;
237+
this.vm = vm;
238+
this.exp = exp;
239+
this.attr = attr;
240+
241+
242+
// 更新操作
243+
this._update();
244+
}
245+
246+
_update() {
247+
this.el[this.attr] = this.vm.$data[this.exp];
248+
}
249+
}
250+
```
251+
252+
`_compile`中,我们创建了两个`Watcher`实例,不过这两个对应的`_update`的操作结果不同,对于`div.text`的操作其实是`div.innerHTML = this.data.myText`,对于`input`的操作相当于`input.value = this.data.myText`,这样每次数据进行`set`操作时,我们会触发两个`_update`方法,分别更新`div``input`的内容。
253+
254+
![udbfdx.gif](https://s2.ax1x.com/2019/10/02/udbfdx.gif)
255+
256+
Finally,我们成功地实现了一个简单的双向绑定。
257+
258+
259+
260+
> 示例Demo源码可在 [Observer](<https://github.com/Reaper622/JavaScript-DesignPatterns/tree/master/Observer>) 查看
261+
262+
### 总结
263+
264+
- 观察者模式可以使代码解耦合,满足开放封闭原则。
265+
- 当过多地使用观察者模式时,如果订阅消息一直没有触发,但订阅者仍然一直保存在内存中。

Observer/Two-way-binding.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>用观察者模式实现双向绑定</title>
6+
</head>
7+
<body>
8+
<div id="app">
9+
<h3>数据的双向绑定</h3>
10+
<div class="cell">
11+
<div class="text" v-text="myText"></div>
12+
<input class="input" type="text" v-model="myText" >
13+
</div>
14+
</div>
15+
<script src="./Two-way-binding.js"></script>
16+
<!-- <script src="./test.js"></script> -->
17+
</body>
18+
</html>

0 commit comments

Comments
 (0)