javaScript设计模式-观察者

Observer (观察者) 模式

一个对象(称为 subject)维持一系列依赖于它(观察者)的对象,将有关状态的任何变更自动通知它们
可以使用以下组件来实现 Oberver 模式

  1. Subject(目标)
    维护一系列的观察者,方便添加或删除观察者
  2. Observer(观察者)
    为那些在目标状态发生改变时需要获得通知的对象提供一个更新接口
  3. ConcreteSubject(具体目标)
    状态发生改变时,向 Observer 发出通知,储存 ConcreteObserver 的状态
  4. ConcreteObserver(具体观察者)
    存储一个指向 ConcreteSubject 的引用,实现 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
function ObserverList() {
this.observerList = [];
}

ObserverList.prototype.Add = function(obj) {
return this.observerList.push(obj);
};

ObserverList.prototype.Empty = function() {
this.observerList = [];
};

ObserverList.prototype.Count = function() {
return this.observerList.length;
};

ObserverList.prototype.Get = function(index) {
if (index > -1 && index < this.observerList.length) {
return this.observerList[index];
}
};

ObserverList.prototype.Insert = function(obj, index) {
var pointer = -1;

if (index === 0) {
this.observerList.unshift(obj);
pointer = index;
} else if (index === this.observerList.length) {
this.observerList.push(obj);
pointer = index;
}

return pointer;
};

ObserverList.prototype.IndexOf = function(obj, startIndex) {
var i = startIndex,
pointer = -1;
var len = this.observerList.length;
while (i < len) {
if (this.observerList[i] == obj) pointer = i;
i++;
}
return pointer;
};

ObserverList.prototype.RemoveIndexAt = function(index) {
if (index === 0) {
this.observerList.shift();
} else if (index === this.observerList.length - 1) {
this.observerList.pop();
}
};

function extend(obj, extension) {
for (var key in obj) {
extension[key] = obj[key];
}
}

接下来,让我们模拟目标(Subject)和在观察者列表上添加、删除或通知观察者的能力

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
function Subject() {
// 订阅者的实例是
this.observers = new ObserverList();
}

Subject.prototype.AddObserver = function(observer) {
this.observers.Add(observer);
// console.log(this.observers)
};

Subject.prototype.RemoveObserver = function(observer) {
this.observers.RemoveIndexAt(this.observers.IndexOf(observer, 0));
};

Subject.prototype.Notify = function(context) {
var observerCount = this.observers.Count();
for (var i = 0; i < observerCount; i++) {
this.observers.Get(i).Update(context);
}
};
// console.log(new Subject())
/* 每一个订阅者对象都会有如下的属性
*
* Subject {observers: ObserverList}
observers: ObserverList
observerList: []
__proto__: 这里是观察者的原型链引用,上面都是Observer 原型链上的函数
Add: ƒ (obj)
Count: ƒ ()
Empty: ƒ ()
Get: ƒ (index)
IndexOf: ƒ (obj, startIndex)
Insert: ƒ (obj, index)
RemoveIndexAt: ƒ (index)
constructor: ƒ ObserverList()
__proto__: Object
__proto__: 这里是订阅者的原型链的引用,上面都是Subject 原型链上的函数
AddObserver: ƒ (observer)
Notify: ƒ (context)
RemoveObserver: ƒ (observer)
constructor: ƒ Subject()
__proto__: Object
* */

function Observer() {
this.Update = function() {
console.log("update");
};
// console.log("Observer 被调用了")
}

如下是 HTML 代码:

1
2
3
<button id="addNewObserver">Add Observer checkbox</button>
<input id="mainCheckbox" type="checkbox" />
<div id="observersContainter"></div>

如下是样例脚本:

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
var controlCheckbox = document.getElementById("mainCheckbox");
var addBtn = document.getElementById("addNewObserver");
var container = document.getElementById("observersContainter");

extend(new Subject(), controlCheckbox);

controlCheckbox["onClick"] = new Function(
"controlCheckbox.Notify(controlCheckbox.checked)"
);

addBtn["onclick"] = AddNewObserver;

function AddNewObserver() {
var check = document.createElement("input");
check.type = "checkbox";

// 把Observer的所有函数挂载到check上--Update
extend(new Observer(), check);

check.Update = function(value) {
this.checked = value;
};

// controlCheckbox继承了 new Subject() 的所有属性、方法
// 把check这个 node 节点添加到观察者数组中 --> this.observerList = [];
controlCheckbox.AddObserver(check);
// 在容器中渲染这个组件
container.appendChild(check);
}

Observer(观察者) 模式和 Publish/Subscribe(发布/订阅)模式的区别

Observer 模式要求希望接收到主题通知的观察者(或对象),必须订阅内容改变的事件

Publish/Subscribe 模式使用了一个主题/事件通道,这个通道介于希望接收到通知(订阅者)的对象和激活事件的对象(发布者)之间。该事件系统允许代码定义应用程序的特定事件,这些事件可以传递自定义参数,自定义参数包含订阅者所需的值。其目的是避免订阅者和发布者之间产生依赖关系。
这与 Observer 模式不同,因为它允许任何订阅者执行适当的事件处理程序来组测和接收发布者发出的通知。

下面这个示例说明了如果有 publish()、subscribe() 和 unsubscribe() 的功能实现,是如何使用 Publish/Subscribe 模式的:

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
// 非常简单的 meail 处理程序

接收到消息的数量;
var mailCounter = 0;

// 初始化订阅,名称是 inbox/newMessage

// 呈现消息预览
var subscriber1 = subscribe("inbox/newMessage", function(topic, data) {
// 打印模式记录 topic
console.log("A new message was received: " + topic);

// 使用从目标subject传递过来的data,一般呈现消息预览
$(".messageSender").html(data.sender);
$(".messagePreview").html(data.body);
});

// 另外一个订阅,使用同样的data数据用于不同的任务

// 通过发布者更新所有接收消息的数量
var subscriber2 = subscribe("inbox/newMessage", function(topic, data) {
$(".newMessageCounter").html(mailCounter++);
});

publish("inbox/newMessage", [
{
sender: "hello@google.com",
body: "Hey there! How are you doing today?"
}
]);

// 之后可以通过unsubscribe来取消订阅
// unsubscribe( subscribe1, );
// unsubscribe( subscriber2 );

优点:
可以将应用程序分解为更小、更松散耦合的块,以改进代码管理和潜在的复用。

使用 Observer 模式背后的另一个动机是我们需要在哪里维护相关对象之间的一致性,而无需使类紧密耦合。例如,当一个对象需要能够通知其他对象,而无需在这些对象方面做假设时。

缺点:
例如,发布者可能会假设:一个或多个订阅者在监听它们。倘若我们假设订阅者需要记录或输出一些与应用程序处理有关的错误。如果订阅者执行日志崩溃了(或出现某种原因不能正常运行),由于系统解耦特性,发布者就不会看到这一点。

这个模式的另一个缺点是:订阅者非常无视彼此的存在,并对变换发布者产生的成本视而不见。由于订阅者和发布者之间的动态关系,很难跟踪依赖更新。

Publish/Subscribe 实现

Publish/Subscribe 非常适用于 JavaScript 生态系统,这主要是因为在其核心,ECMAScript 实现是由事件驱动的。在浏览器环境下尤其如此,因为 DOM 将事件是作为脚本编程的主要交互 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

// jQuery: $(obj).trigger('channel', [arg1, arg2, arg3]) -- 发布
$(el).trigger('/login', [{username: 'test', userData: 'test'}]); -
// jQuery: $(obj).on("channel", [data], fn); -- 订阅
$(el).on("/login", function(event) { ... });
// jQuery: $(obj).off("channel") -- 取消订阅
$(el).off("/login")

// Dojo: dojo.publish('channel', [arg1, arg2, arg3]) -- 发布
dojo.puhlish("/login", [{username: 'test', userData: 'test'}]);
// Dojo: dojo.subscribe("channel", fn) -- 订阅
var handle = dojo.subscribe("/login", function(data) { ... });
// Dojo: dojo.unsubscribe(handle) -- 取消订阅
dojo.unsubscribe(handle);

// YUI: el.publish('channel', [arg1, arg2, arg3]) -- 发布
el.publish("/login", {username: 'test', userData: 'test'});
// YUI: el.on("channel", handler); -- 订阅
el.on("/login", function(data) { ... });
// YUI: el.detach("channel"); -- 取消订阅
el.detach("/login");

Publish/Subscribe 实现

  1. 先定义一个对象
  2. 通过立即执行函数给这个对象加载三个函数: publish, subscribers, unscribers
  3. 通过这个对象订阅一个主题
  4. 通过这个对象发布一个主题,订阅者发现这个主题有更新,调用内部函数处理更新的数据。
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
var pubsub = {};

(function(q) {
var topics = {},
subUid = -1;

q.publish = function(topic, data) {
if (!topics[topic]) {
return false;
}

var subscribers = topics[topic],
len = subscribers ? subscribers.length : 0;

while (len--) {
subscribers[len].func(topic, data);
}
return this;
};

q.subscriber = function(topic, func) {
if (!topics[topic]) {
topics[topic] = [];
}

var token = (++subUid).toString();
topics[topic].push({
token,
func
});

return token;
};

q.unsubscribe = function(token) {
for (var m in topics) {
if (topics[m]) {
for (var i = 0, j = topics[m].length; i < j; i++) {
if (topics[m][i].token === token) {
topics[m].splice(i, 1);
return token;
}
}
}
}
return this;
};
})(pubsub);

var messageLogger = function(topics, data) {
console.log(topics + ": ", data);
};

var subscription = pubsub.subscriber("inbox/newMessage", messageLogger);
var subscription = pubsub.subscriber("inbox2/newMessage2", messageLogger);

pubsub.publish("inbox/newMessage", "world1");
pubsub.publish("inbox/newMessage", "world11");
pubsub.publish("inbox2/newMessage2", "world2");

用户界面通知

接下来,假设我们有一个负责显示实时股票信息的 Web 应用程序。

该应用程序有一个显示股票统计和网格和一个显示最后更新点的计数器。当数据模型改变时,应用程序需要更新网格和计数器。在这种情况下,目标(它将发布主题/通知)就是数据模型,观察者就是网格和计数器。

再我们的实现中,订阅者会监听 newDataAvailable 这个 topic,以探测是否有新的股票信息。如果新通知发布到这个 topic,他将触发 gridUpdate 向包含股票信息的网格添加一个新行。他还将更新一个 last updated 计数器来记录最后一次添加的数据

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

var grid = {
// 向网格组件上添加新数据行
addGridRow: function(data) {
console.log("update grid comonent with: ", data);
},
// 更新网格上的最新更新时间
updateCounter: function(data) {
console.log("data last updated at: " + getCurrentTime() + " with " + data);
}
};

// 在newDataAvailable topic上创建一个订阅
var subscriber = pubsub.subscriber("newDataAvailable", gridUpdate);

function gridUpdate(topic, data) {
if (data !== "undefined") {
grid.addGridRow(data);
grid.updateCounter(data);
}
}
// 返回稍后界面上要用到的当前本地时间
getCurrentTime = function() {
var date = new Date(),
m = date.getMonth(),
d = date.getDate(),
y = date.getFullYear(),
t = date.toLocaleTimeString().toLowerCase();

return m + "/" + d + "/" + y + " " + date;
//data last updated at: 7/19/2019 Mon Aug 19 2019 22:37:11 GMT+0800 (中国标准时间) with [object Object]
};

// 下面的代码描绘了数据层,一般应该使用ajax请求获取最新的数据后,告知程序有最新数据
// 发布者更新gridUpdate topic 来展示新数据项
pubsub.publish("newDataAvailable", {
summary: "Apple made $5 billion",
identifier: "appl",
stockPrice: 1000.0
});

pubsub.publish("newDataAvailable", {
summary: "Microsoft made $20 million",
identifier: "msft",
stockPrice: 30.85
});

使用Ben Alman 的 Pub/Sub 实现解耦应用程序

我们将使用Ben Alman在Publish/Subscribe模式上的jQuery实现来展示如何解耦一个用户界面。

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
   <!-- html模板 -->
<script id="userTemplate" type="text/html">
<li><%= name %></li>
</script>

<script id="ratingTemplate" type="text/html">
<li><strong><%= title %></strong> was rated <%= rating %> 5 </li>
</script>

<div class="sampleForm">
<p>
<label for="twitter_handle">Twitter handle:</label>
<input type="text" id="twiter_handle" />
</p>
<p>
<label for="movie_seen">Name a movie you've seen this year:</label>
<input type="text" id="movie_seen" />
</p>
<p>
<label for="movie_rating">Rate the movie you saw:</label>
<select id="movie_rating">
<option value="b">a</option>
<option value="l">b</option>
<option value="u">c</option>
<option value="e">d</option>
</select>
</p>
<p>
<button id="add">Submit rating</button>
</p>

<div class="summaryTable">
<div id="users">
<h3>Recent users</h3>
</div>
<div id="ratings">
<h3>Recent Movies rated</h3>
</div>
</div>
</div>
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
;(function($) {

var o = $({}); //jQuery.fn.init [{…}]

$.subscribe = function() {
o.on.apply(o, arguments);
};

$.unsubscribe = function() {
o.off.apply(o, arguments);
};

$.publish = function() {
o.trigger.apply(o, arguments);
};

}(jQuery));

; (function($){
// 订阅 new user主题,提交评论的时候在用户列表添加一个用户
$.subscribe('/new/user', function(e, data) {
var compiledTemplate;
if(data) {
compiledTemplate = _.template($('#userTemplate').html());
$('#users').append(compiledTemplate(data));
}
})
// 订阅new rating主题,rating主题由title 和 rating组成,新rating添加到已有用户的rating列表上。
$.subscribe('/new/rating', function(e, data) {
var compiledTemplate;
if(data) {
compiledTemplate = _.template($('#ratingTemplate').html());
$('#ratings').append(compiledTemplate(data));
}
})
// 添加新用户处理程序
$('#add').on('click', function(e) {
e.preventDefault();

var strUser = $('#twiter_handle').val(),
strMovie = $('#movie_seen').val(),
strRating = $('#movie_rating').val();

// 通知程序,新用户有效
$.publish('/new/user', {name: strUser});
// 通知程序新rating评价有效
$.publish('/new/rating', {title: strMovie, rating: strRating });
});






})(jQuery)

解耦基于Ajax的jQuery应用程序

如何使用Pub/Sub解耦在早期开发过程中的代码,以使我们省去一些可能繁琐的重构工作

下面的样例中,当用户表示他想进行搜索查询时,是如何发出一个topic通知的,以及当请求返回并且实际数据可用时,是如何发出另一个通知的。它让订阅者随后决定如何利用这些时间(或返回的数据)。它的好处是:如果我们愿意,我们可以有10个不同的订阅者以不同的方式使用返回的数据,但对于Ajax层面而言是无关紧要的。其唯一的责任是请求和返回数据,然后传递给任何一个想使用它的人。

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
<form id="flickrSearch">

<input type="text" name="query" id="query" value="" />
<input type="submit" name="submit" id="submit" value="submit" />
</form>

<div id="lastQuery">

</div>

<div id="searchResults">

</div>

<script id="resultTemplate" type="text/html">
<% _.each(forecast, function(item) {%>
<li>
<div id="">
<%= item.date %>
<%= item.fengli %>
<%= item.fengxiang %>
<%= item.high %>
<%= item.low %>
<%= item.type %>
</div>
</li>
<% });%>
</script>
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($) {

var o = $({});
$.subscribe = function() {
o.on.apply(o, arguments);
};

$.unsubscribe = function() {
o.off.apply(o, arguments);
};

$.publish = function() {
o.trigger.apply(o, arguments);
};

}(jQuery));

;(function($){

// 预编译模板,并使用闭包缓存它
var html = $("#resultTemplate").html()

var resultTemplate = _.template(html);
// resultTemplate = ƒ (n){return t.call(this,n,h)}

// 订阅新搜索 tags 主题
$.subscribe("/search/tags", function(e, tags) {
$("#searchResults").html('search for: ' + tags + "");

});

// 订阅新搜索结果主题
$.subscribe("/search/resultSet", function(e, results) {

var a = resultTemplate(results);
$('#searchResults').append(a);
$("#lastQuery").html(results)
});

// 提交搜索请求,并在/search/tags 主题上发布tags
$("#flickrSearch").submit(function(e) {
e.preventDefault();
var tags = $(this).find("#query").val();

if(!tags) {
return;
}

$.publish("/search/tags", [$.trim(tags)]);
});
// 订阅发布的新tag,并且使用tag发起请求,一旦返回数据,将数据发布给应用程序的其他使用者
$.subscribe("/search/tags", function(e, tags) {
$.getJSON("https://www.apiopen.top/weatherApi?city=成都", function(res) {
if(data){
$.publish("/search/resultSet", res.data)
}
});
});
})(jQuery)