您的位置:首页 > 其它

观察者(发布——订阅)模式

2015-10-10 22:14 218 查看

观察者模式

  观察者模式广泛应用于客户端JavaScript编程中。所有的浏览器事件(鼠标悬停,按键等事件)是该模式的例子。它的另一个名字也称自定义事件,与那些由浏览器触发的相比,自定义事件表示是由你编程实现的事件。此外,该模式的另一个别名是订阅——发布模式

  设计这种模式背后的主要动机是促进形成松散耦合。在这种模式中,并不是一个对象调用另一个对象的方法,而是一个对象订阅另一个对象的特定活动并在状态改变后获得通知。订阅者也称之为观察者,而被观察者的对象称为发布者或主题。当发生了一个重要的事件时,发布者将会通知(调用)所有订阅者并且可能经常以事件对象形式传递消息。

1. 现实中的观察者模式

  以售楼处为例,小明想要买房,于是招待人员记下小明的手机。小兵,小龙也买房,招待人员获得他们的手机通通给记在花名册上,过几天,有了他们中意的房子,工作人员便会翻开花名册,打电话伺候。

  在这个例子中,小明,小龙,小兵是订阅者,他们订阅房子的信息。售楼处是发布者,一有消息便会依次打电话给购房者。

2. DOM事件

  实际上,只要我们曾经在DOM节点上面绑定过事件函数,那我们就曾经使用过观察者模式,来看看下面这两句简单的代码发生了什么事情:

document.body.addEventListener('click', function () {
alert('1');
}, false);
document.body.click();//模拟用户点击

  在这里需要监控用户点击document.body的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息。

3. 自定义事件

  除了DOM事件,我们还会经常实现一些自定义的事件,这种依靠自定义事件完成的观察者模式可以用于任何JS代码中。

  现在看看如何一步步实现观察者模式:

首先要指定好谁充当发布者(比如售楼处)

然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(比如花名册)

最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(打开花名册,依次打电话)

  另外,我们还可以往回调函数里填入一些参数,订阅者可以接受这些参数。这是很有必要的,比如售楼处可以在发给订阅者的短信里加上房子的单价,面积等信息,订阅者接受到这些信息之后可以进行各自的处理:

var salesOffices = {};  //定义售楼处
salesOffices.clientList = [];   //缓存列表,存放订阅者的回调函数
salesOffice.listen = function (fn) {    //增加订阅者
this.clientList.push(fn);   //订阅的消息添加进缓存列表
};
salesOffice.trigger = function () { //发布消息
for (var i = 0, fn; fn = this.clientList[i++];) {
fn.apply(this, arguments); //arguments是发布消息时带上的参数
}
};

  下面我们来进行一些简单的测试:

var salesOffices = {};  //定义售楼处
salesOffices.clientList = [];   //缓存列表,存放订阅者的回调函数
salesOffices.listen = function (fn) {    //增加订阅者
this.clientList.push(fn);   //订阅的消息添加进缓存列表
};
salesOffices.trigger = function () { //发布消息
for (var i = 0, fn; fn = this.clientList[i++];) {
fn.apply(this, arguments); //arguments是发布消息时带上的参数
}
};
salesOffices.listen(function (price, squareMeter) { //小明订阅消息
console.log('a价格= ' + price);
console.log('squareMeter= ' + squareMeter);
});
salesOffices.listen(function (price, squareMeter) { //小龙订阅消息
console.log('b价格= ' + price);
console.log('squareMeter= ' + squareMeter);
});
salesOffices.trigger(2000000, 88);
salesOffices.trigger(3000000, 110);
/*输出:
a价格= 2000000
squareMeter= 88
b价格= 2000000
squareMeter= 88
a价格= 3000000
squareMeter= 110
b价格= 3000000
squareMeter= 110
*/

  至此,我们已经实现了一个最简单的观察者模式,但这里还存在一些问题。我们看到订阅者接收到了发布者发布的每个消息,虽然小明只想买88平米的房子,但是发布者把110平米的信息也推送给了小明,这对小明来说很是麻烦。所以我们有必要增加一个标示key,让订阅者只订阅自己感兴趣的消息。改写后的代码如下:

var salesOffices = {};  //定义售楼处
salesOffices.clientList = [];   //缓存列表,存放订阅者的回调函数
salesOffices.listen = function (key, fn) {    //增加订阅者
if (this.clientList[key] === undefined) {   //如果还没有订阅过此类消息,给该类消息创建一个缓存列表
this.clientList[key] = [];
}
this.clientList[key].push(fn);   //订阅的消息添加进缓存列表
};
salesOffices.trigger = function () { //发布消息
var key = Array.prototype.shift.call(arguments);    //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns && fns.length === 0) { //如果没有订阅该消息,则返回
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments); //arguments是发布消息时带上的参数
}
};
salesOffices.listen('squareMeter88', function (price) { //小明订阅88平米房子的消息
console.log('a价格= ' + price);
});
salesOffices.listen('squareMeter110', function (price) { //小龙订阅110平米房子的消息
console.log('b价格= ' + price);
});
salesOffices.trigger('squareMeter88', 2000000);
salesOffices.trigger('squareMeter110', 3000000);
/*输出:
a价格= 2000000
b价格= 3000000
*/

  现在,订阅者可以只订阅自己感兴趣的事了。

4. 观察者模式的通用实现

  现在我们已经看到了如何让售楼处拥有接受订阅和发布事件的功能。假设现在小明又去另一个售楼处买房子,那么这段代码是否必须在另一个售楼处对象上重写一次呢,有没有办法可以让所有对象都拥有发布——订阅功能呢?

  答案是有的,JavaScript作为一门解释执行的语言,给对象动态添加职责是理所当然的事情。

  所以我们把发布——订阅的功能提取出来,放在一个单独的对象内:

var event = {
clientList: [],
listen: function (key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);
},
trigger: function () {
var key = Array.prototype.shift.call(arguments);
var fns = this.clientList[key];
if (!fns && fns.length === 0) {
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
}
};

  再定义一个installEvent函数,这个函数可以给所有的对象都动态安装发布——订阅功能:

var installEvent = function (obj) {
for (var i in event) {
obj[i] = event[i];
}
};

  再来测试,我们给售楼处对象salesOffice是动态增加发布——订阅功能:

var salesOffices = {};
installEvent(salesOffices);
salesOffices.listen('squareMeter88', function (price) {
console.log('a价格= ' + price);
});
salesOffices.listen('squareMeter110', function (price) {
console.log('b价格= ' + price);
});
salesOffices.trigger('squareMeter88', 2000000);     //输出:2000000
salesOffices.trigger('squareMeter110', 3000000);    //输出:3000000

5. 取消订阅事件

  有时候,我们也许需要取消订阅事件的功能。比如小明突然不想买房子了,为了避免继续接到售楼处的电话,小明需要取消之前订阅的事件。现在我们给event对象增加remove方法:

event.remove = function (key, fn) {
var fns = this.clientList[key];
if (!fns) { //如果key对应的消息没有被人订阅,则直接返回
return false;
}
if (!fn) {  //如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
fns.length = 0;
}else{
for (var i = 0, _fn; _fn = fns[i]; i++) {
if (_fn === fn) {
fns.splice(i, 1);   //删除订阅者的回调函数
}
}

}
};
var installEvent = function (obj) {
for (var i in event) {
obj[i] = event[i];
}
};
var salesOffices = {};
installEvent(salesOffices);
salesOffices.listen('squareMeter88', fn1 = function (price) {
console.log('a价格= ' + price);
});
salesOffices.listen('squareMeter88', fn2 = function (price) {
console.log('b价格= ' + price);
});
salesOffices.remove('squareMeter88', fn1);
salesOffices.trigger('squareMeter88', 2000000);     //输出:2000000

6. 全局的发布——订阅对象

  在现实中,买房子未必要亲自去售楼处,我们只要把订阅的请求交给中介公司,而各大房产公司也只需要通过中介公司来发布房子信息。这样一来,我们不用关心消息是来自哪个房产公司,我们在一的是能否顺利接受消息。当然,为了保证订阅者和发布者能顺利通信,订阅者和发布者都必须知道这个中介公司。

  同样在程序中,发布——订阅模式可以用一个全局的Event对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event作为一个类似“中介者”的角色,把订阅者和发布者联系起来。见如下代码:

var Event = (function () {
var clientList = {},
listen,
trigger,
remove;
listen = function (key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
};
trigger = function () {
var key = Array.prototype.shift.call(arguments);
var fns = clientList[key];
if (!fns && fns.length === 0) {
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
};
remove = function (key, fn) {
var fns = this.clientList[key];
if (!fns) {
return false;
}
if (!fn) {
fns.length = 0;
}else{
for (var i = 0, _fn; _fn = fns[i]; i++) {
if (_fn === fn) {
fns.splice(i, 1);
}
}

}
};
return {
listen: listen,
trigger: trigger,
remove: remove
};
})();
Event.listen('squareMeter88', function(price) {
console.log('a价格= ' + price);
});
Event.trigger('squareMeter88', 2000000);     //输出:2000000

7. 模块间的通信

  上一节中实现的发布——订阅模式的实现,是基于一个全局的Event对象,我们利用它可以在两个封装良好的模块中进行通信,这两个模块可以完全不知道对方的存在。就如同有了中介公司之后,我们不再需要知道房子的消息来自哪个售楼处。

  比如现在有两个模块,a模块里面有一个按钮,每次点击按钮之后,b模块里的div中会显示按钮的总点击次数,我们用全局发布——订阅模式完成下面的代码,使得a模块和b模块可以在保持封装性的前提下进行通信。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
</head>
<body>
<button id="count">dian我</button>
<div id="div"></div>
<script>
var a = (function () {
var count = 0;
var button = document.getElementById('count');
button.onclick = function() {
Event.trigger('add', count++);
};
})();
var b = (function () {
var div = document.getElementById('div');
Event.listen('add', function (count) {
div.innerHTML = count;
});
})();
</script>
</body>
</html>

  但在这里我们要留意另一个问题,模块之间用了太多的全局发布——订阅模式来通信,那么模块与模块之间的联系就被隐藏到了背后。我们最终会搞不清楚消息来自哪个模块,或者消息会流向哪些模块,这又会给我们的维护带来一些麻烦,也许某个模块的作用就是暴露一些借口给其他模块调用。

  参考书目:《JavaScript模式》,《JavaScript设计模式与开发实践》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: